1
0
ответвлено от main/neurocomputers-python
Files
neurocomputers-python/lab1/1.1_numpy_solution.ipynb

2248 строки
105 KiB
Plaintext
Исходник Постоянная ссылка Ответственный История

Этот файл содержит невидимые символы Юникода
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### ЛАБОРАТОРНАЯ РАБОТА №1.1\n",
"## ИЗУЧЕНИЕ ОСНОВНЫХ ПОНЯТИЙ ТЕОРИИ ИСКУССТВЕННЫХ НЕЙРОННЫХ СЕТЕЙ"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> Цель работы: изучение основных понятий теории искусственных нейронных сетей на примере простых задач распознавания логических функций («И», «ИЛИ», «исключающее ИЛИ»)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Импорт библиотек:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from IPython.display import clear_output\n",
"\n",
"%matplotlib inline"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В первой части данной лабораторной работы мы используем библиотеку NumPy (стандартный псевдоним при импорте — `np`), которая является фундаментальным пакетом для научных вычислений в Python. Она предоставляет:\n",
"- многомерные массивы (`ndarray`) с эффективной реализацией;\n",
"- набор математических функций для работы с массивами;\n",
"- инструменты для линейной алгебры, Фурье‑преобразований и генерации случайных чисел.\n",
"\n",
"Полную информацию о библиотеке и её возможностях вы найдёте в официальной документации: \n",
"[https://numpy.org/doc/](https://numpy.org/doc/)\n",
"\n",
"Кроме того, чтобы быстро, с помощью встроенной справки узнать подробности о конкретном методе или объекте NumPy, используйте символ `?` перед его именем.\n",
"\n",
"Например, для получения справки по функции создания массивов `array` напишите в ячейке кода `?np.array`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Ваш код здесь"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Содержание: \n",
"\n",
"[1. Работа искусственного нейрона](#p_1) \n",
"[2. Обратное распространение ошибки](#p_2) \n",
"[3. Решение задачи логического «И»](#p_3) \n",
"[4. Решение задачи логического «ИЛИ»](#p_4) \n",
"[5. Решение задачи логического «исключающего ИЛИ»](#p_5) "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Работа искусственного нейрона<a id=\"p_1\"></a>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим работу искусственной нейронной сети, состоящей из одного нейрона, на примере задачи распознавания логической операции «И».\n",
"\n",
"Требуется спроектировать и обучить искусственный нейрон таким образом, чтобы он корректно воспроизводил поведение логической функции «И»: на выходе нейрона должно формироваться значение `1` (истина) только в том случае, если оба входных сигнала равны `1`. Во всех остальных случаях выход должен быть `0` (ложь).\n",
"\n",
"**Входные данные:** \n",
"На вход нейрона подаются двоичные векторы длины2 — все возможные комбинации значений двух логических переменных:\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"&\\mathbf{x}_1 = [0,\\, 0], \\\\\n",
"&\\mathbf{x}_2 = [0,\\, 1], \\\\\n",
"&\\mathbf{x}_3 = [1,\\, 0], \\\\\n",
"&\\mathbf{x}_4 = [1,\\, 1].\n",
"\\end{aligned}\n",
"$$\n",
"\n",
"**Требуемые выходные значения (целевые метки):** \n",
"Для каждого входного вектора ожидаемый выход $y$ определяется по таблице истинности операции «И»:\n",
"\n",
"$$\n",
"\\begin{array}{c|c|c}\n",
"x_1 & x_2 & y \\\\\n",
"\\hline\n",
"0 & 0 & 0 \\\\\n",
"0 & 1 & 0 \\\\\n",
"1 & 0 & 0 \\\\\n",
"1 & 1 & 1 \\\\\n",
"\\end{array}\n",
"$$\n",
"\n",
"**Формальная постановка задачи:** \n",
"Необходимо найти такие параметры нейрона: \n",
"- вектор весов $\\mathbf{W} = [w_1,\\, w_2]$, \n",
"- смещение (порог) $b$, \n",
"\n",
"чтобы для любого входного вектора $\\mathbf{x} = [x_1,\\, x_2]$ выполнялось:\n",
"\n",
"$$\n",
"y_{\\text{pred}} = f(w_1 x_1 + w_2 x_2 + b) \\approx y,\n",
"$$\n",
"\n",
"где $f$ — функция активации (подробнее будет рассмотрено ниже)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим, как входные данные проходят через искусственный нейрон: от подачи на вход до вычисления выходного значения — или прямой проход.\n",
"\n",
"Создадим массив (матрицу) `X_data`, содержащий все возможные комбинации входных значений для логической операции «И» (два входа, каждый — `0` или `1`). В ней всего 4 комбинации, соответствующие таблице истинности операции «И»."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"X_data = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])\n",
"print(X_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Для обучения нейрона необходимы «правильные» ответы — значения, которые модель должна выдавать для каждого входного вектора. В случае логической операции «И» (логическое умножение) правило следующее:\n",
"- выход равен `0`, если хотя бы один из входов равен `0`;\n",
"- выход равен `1`, только если оба входа равны `1`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"y_and_data = np.array([0, 0, 0, 1]).reshape(-1, 1)\n",
"print(y_and_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Для работы нейрона необходимо задать начальные значения его параметров (т.е. инициализировать их):\n",
"- весов (`w`) — коэффициентов, определяющих вклад каждого входного признака;\n",
"- смещения (`b`) — порогового значения, сдвигающего активацию нейрона.\n",
"\n",
"Рассмотрим пример равномерного распределения для инициализации весов и смещения:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"np.random.seed(seed=21)\n",
"\n",
"w = np.random.uniform(low=-.05, high=.05, size=(2, 1))\n",
"b = np.random.uniform(low=-.05, high=.05)\n",
"\n",
"print('Веса:\\n', w)\n",
"print('\\nСмещение:', b)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Самостоятельно изучите функцию `np.random.normal` и реализуйте инициализацию весов и смещения на её основе. Обратите внимание на ключевые параметры:\n",
" - `loc` — среднее значение распределения (математическое ожидание);\n",
" - `scale` — стандартное отклонение (определяет разброс значений);\n",
" - `size` — форма выходного массива."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"w_norm = # Ваш код здесь\n",
"b_norm = # Ваш код здесь\n",
"\n",
"# Ваш код здесь"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"*Примечание*. `np.random.seed(seed)` — функция NumPy для установки начального значения (seed) генератора псевдослучайных чисел.\n",
"\n",
"Установка seed обеспечивает:\n",
" - воспроизводимость результатов - при каждом запуске кода будут генерироваться *одни и те же* случайные числа;\n",
" - контролируемость экспериментов - позволяет сравнивать результаты разных запусков при неизменных начальных условиях."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Умножим матрицу входных данных `X_data` на вектор весов `w`, чтобы вычислить взвешенную сумму входов для каждого примера, затем прибавим смещение `b` — так мы получим линейную комбинацию входных признаков, которая послужит входным сигналом для функции активации нейрона."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"s = X_data.dot(w) + b\n",
"print(s)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"*Примечание*. В NumPy для умножения векторов и матриц можно использовать три эквивалентных способа: функцию `np.dot()`, метод массива `np.dot()` и оператор `@`. Все три способа выполняют скалярное (точечное) произведение векторов или матричное умножение. Для матриц результат соответствует правилам матричного умножения."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Перейдём к функциям активации.\n",
"\n",
"Функции активации вводят нелинейность в работу нейрона. Без них нейронная сеть сводилась бы к линейной модели (независимо от числа слоёв), что резко ограничивало бы её возможности — она не смогла бы обучаться сложным зависимостям в данных.\n",
"\n",
"Рассмотрим сигмоиду как наглядный пример функции активации:\n",
"\n",
"$$\n",
"\\sigma(s) = \\frac{1}{1 + e^{-s}},\n",
"$$\n",
"\n",
"Она преобразует любое вещественное число `s` в значение из интервала (0, 1). Это удобно для:\n",
"- интерпретации выхода как вероятности;\n",
"- бинарной классификации (выход близко к 0 — класс «нет», близко к 1 — класс «да»)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def sigmoid(s, c=1):\n",
" return 1. / (1. + np.exp(-c*s))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Функции активации должны обладать следующими ключевыми свойствами: \n",
"- нелинейность — позволяет нейронной сети обучаться сложным зависимостям. \n",
"- дифференцируемость — можно вычислять градиенты для обучения методом обратного распространения ошибки;\n",
"- ограниченность (для нейронов выходных слоёв в некоторых задачах) — например, выход всегда в диапазоне (0, 1), что удобно для интерпретации как вероятности. \n",
"\n",
"Самостоятельно реализуйте несколько функций активации:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def tanh(s):\n",
" return # Ваш код здесь\n",
"\n",
"# Ваш код здесь"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Постройте графики реализованных вами функций активации (по примеру строки кода для сигмоиды):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"ss = np.linspace(start=-10, stop=10, num=200)\n",
"\n",
"plt.plot(ss, sigmoid(ss), label='sigmoid')\n",
"plt.plot(# Ваш код здесь)\n",
"# Ваш код здесь\n",
"plt.legend(loc='best')\n",
"plt.grid(True)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Дополнительно рассмотрим такой важный параметр функции сигмоиды, как температуру (в коде обозначен `c`). Хотя в классической формуле сигмоиды этот параметр отсутствует, его введение позволяет гибко регулировать форму активационной функции.\n",
"\n",
"Температура `c` выступает положительным множителем при входном сигнале `s` в экспоненте: \n",
"$$\n",
"\\sigma(s, c) = \\frac{1}{1 + e^{-c \\cdot s}}\n",
"$$ \n",
"Очевидно, что при `c = 1` функция совпадает со стандартной сигмоидой. \n",
"\n",
"В ячейке с кодом ниже по примеру для `c = 1` постройте графики для вариантов, когда:\n",
" - температура больше единицы;\n",
" - температуры меньше единицы. \n",
" \n",
"Проинтерпретируйте результат. Для каких случаях применим каждый из этих двух вариантов?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.plot(ss, sigmoid(ss, c=1), label='1')\n",
"plt.plot(ss, # Ваш код здесь)\n",
"plt.plot(ss, # Ваш код здесь)\n",
"plt.title('Sigmoid')\n",
"plt.legend(loc='best', title='temperature c:')\n",
"plt.grid(True)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Применим сигмоиду как функцию активации к взвешенной сумме входов для каждого примеров - чтобы найти вероятностную оценку, которая и будет выходным сигналом нейрона:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"z = sigmoid(s) \n",
"print(z)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"После вычисления выходного сигнала нейрона `z` возникает главный вопрос: насколько полученный результат близок к истинному значению?\n",
"\n",
"Для ответа используем функцию потерь — в нашем случае для удобства выбрана среднеквадратичная ошибка (Mean Square error, MSE).\n",
"\n",
"Формула MSE:\n",
"$$\n",
"\\mathcal{L}_{MSE} = \\frac{1}{n} \\sum_{i=1}^{n} (y_{\\text{pred}, i} - y_{\\text{true}, i})^2,\n",
"$$\n",
"где:\n",
"- $n$ — количество примеров в выборке;\n",
"- $y_{\\text{pred}, i}$ — предсказание модели для $i$-го примера;\n",
"- $y_{\\text{true}, i}$ — истинное значение для $i$-го примера.\n",
"\n",
"MSE количественно измеряет расхождение (или в геометрическом смысле расстояние) между предсказанными значениями (`y_pred`) и истинными (`y_true`). Чем меньше значение MSE (как и значение любой другой функции потерь), тем лучше построенная нами модель соответствует данным.\n",
"\n",
"Реализация MSE через Numpy:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def mse(y_pred, y_true):\n",
" return np.mean((y_pred - y_true) ** 2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Значение MSE на данный момент:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"mse(z, y_and_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"*Примечание*. MSE, как правило, применяется в задаче регрессии, где нужно предсказать непрерывное значение (например, цену дома или температуру). Однако, как видно, её можно использовать и в задаче классификации — хотя это менее типично, поскольку MSE никак не учитывает вероятностную природу выходных данных.\n",
"\n",
"Для бинарной классификации стандартна бинарная кросс‑энтропия (Binary Cross Entropy) или logloss: \n",
"$$\n",
"\\mathcal{L}_{BCE} = -\\frac{1}{n} \\sum_{i=1}^{n} \\left[ y_i \\log(\\hat{y}_i) + (1 - y_i) \\log(1 - \\hat{y}_i) \\right],\n",
"$$ \n",
"где: \n",
"- $y_i \\in \\{0, 1\\}$ — истинная метка класса, \n",
"- $\\hat{y}_i \\in \\{0, 1\\}$ — предсказанная вероятность принадлежности к классу 1, \n",
"- $n$ — число примеров.\n",
"\n",
"Для многоклассовой классификации используют кросс‑энтропию (Cross Entropy) в общей форме: \n",
"$$\n",
"\\mathcal{L}_{CE} = -\\frac{1}{n} \\sum_{i=1}^{n} \\sum_{k=1}^{K} y_{ik} \\log(\\hat{y}_{ik}),\n",
"$$ \n",
"где: \n",
"- $K$ — число классов, \n",
"- $y_{ik}$ — истинная вероятность (обычно 1 для истинного класса и 0 для остальных), \n",
"- $\\hat{y}_{ik}$ — предсказанная вероятность класса $k$ для примера $i$.\n",
"\n",
"Очевидно, что бинарная кросс‑энтропия — это частный случай многоклассовой при $K=2$.\n",
"\n",
"В отличие от MSE, кросс‑энтропия учитывает вероятностный характер предсказаний."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Поскольку мы решаем задачу классификации, важно оценивать, насколько хорошо модель предсказывает классы (а не просто выдаёт пусть и полезные, но служебные числовые значения).\n",
"\n",
"Для этого используем метрику accuracy (доля правильно классифицированных примеров от общего числа).\n",
"\n",
"Преобразуем выданные нейроном вероятности в классы `0` и `1` (отсекаем по задаваемому порогу — по умолчанию это 0.5). Вектор предсказанных классов для всех примеров:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"model_answer_interpretation = (z >= 0.5).astype('int')\n",
"print(model_answer_interpretation)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Напомним исходные ответы `y_and_data`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(y_and_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Реализация метрики accuracy:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def accuracy(y_pred, y_true):\n",
" return np.sum(y_pred == y_true) / len(y_true)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Значение accuracy на данный момент:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"accuracy(model_answer_interpretation, y_and_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Чтобы снизить значение MSE (уменьшить расхождение между предсказанными и истинными значениями), необходимо подобрать такие значения весов и смещения, при которых ошибки предсказания будут минимальными, а качество классификации — максимальным.\n",
"\n",
"Рассмотренный нами одиночный нейрон представляет собой нейронную сеть самой простейшей архитектуры. Изменение значений его весов и смещения — это задача обучения нейронной сети."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. Обратное распространение ошибки<a id=\"p_2\"></a>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Задачу обучения нейронной сети как задачу оптимизации можно поставить (и проинтерпретировать) следующим образом.\n",
"\n",
"В процессе обучения мы никак не можем изменять структуру сети, входные данные или функцию активации — единственное, что доступно для настройки, это параметры модели: \n",
"- веса связей ($w_{ij}$); \n",
"- смещения ($b_i$). \n",
"\n",
"Тогда наша цель — подобрать такие значения этих параметров, при которых функция потерь $\\mathcal{L}(\\theta)$ (где $\\theta$ — вектор всех параметров) принимает минимально возможное значение на обучающей выборке.\n",
"\n",
"Минимизация функции потерь осуществляется методом градиентного спуска: параметры изменяются в направлении антиградиента функции потерь, то есть в направлении наиболее быстрого убывания $\\mathcal{L}$.\n",
"\n",
"Почему нужен именно антиградиент?\n",
"\n",
"Градиент $\\nabla_{\\theta} \\mathcal{L}$ указывает направление наиболее быстрого возрастания функции, тогда как антиградиент $-\\nabla_{\\theta} \\mathcal{L}$ — направление наиболее быстрого убывания. Поэтому для минимизации мы двигаемся против градиента.\n",
"\n",
"**Итеративное обновление параметров** выполняется по формуле: \n",
"\n",
"$$\n",
"\\theta^{[t+1]} \\leftarrow \\theta^{[t]} - \\eta \\nabla_{\\theta} \\mathcal{L}(\\theta^{[t]}),\n",
"$$ \n",
"где: \n",
"- $\\theta^{[t]}$ — вектор параметров на шаге $t$; \n",
"- $\\theta^{[t+1]}$ — обновлённый вектор параметров; \n",
"- $\\eta > 0$ — скорость обучения (learning rate), скалярный гиперпараметр, определяющий «шаг» движения в пространстве параметров; \n",
"- $\\nabla_{\\theta} \\mathcal{L}$ — градиент функции потерь по параметрам $\\theta$.\n",
"\n",
"Подробное пояснение компонентов:\n",
"\n",
"1. Минус перед градиентом. \n",
" Обеспечивает движение в направлении антиградиента, а не градиента. Без минуса мы бы максимизировали функцию потерь, что противоположно нашей цели.\n",
"\n",
"2. Скорость обучения $\\eta$.\n",
" - Если $\\eta$ слишком велика, алгоритм может «перепрыгнуть» минимум и даже расходиться. \n",
" - Если $\\eta$ слишком мала, обучение будет чрезмерно медленным. \n",
" - На практике $\\eta$ выбирают эмпирически или используют адаптивные методы (Adam, RMSprop).\n",
"\n",
"3. Градиент $\\nabla_{\\theta} \\mathcal{L}$. \n",
" Вектор, составленный из всех частных производных функции потерь $\\mathcal{L}$ по каждому из параметров модели $\\theta$. Если модель имеет набор настраиваемых параметров $\\theta = (\\theta_1, \\theta_2, \\ldots, \\theta_m)$, то градиент объединяет информацию о том, как изменение *каждого* параметра влияет на значение функции потерь. Таким образом, именно значение градиента задаёт направление, в котором следует изменять параметры, чтобы минимизировать ошибку модели."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В случае многослойных нейронных сетей прямое (аналитическое) вычисление частных производных функции потерь по всем параметрам сталкивается с рядом принципиальных сложностей:\n",
"\n",
"1. Экспоненциальный рост числа слагаемых \n",
" При развёртывании производных по цепному правилу количество слагаемых растёт экспоненциально с увеличением числа слоёв. Для сети из нескольких слоёв вычисление $\\frac{\\partial \\mathcal{L}}{\\partial \\theta^{(1)}}$ (производной по параметру первого слоя: например, веса) требует учёта всех путей влияния этого параметра на выход через промежуточные слои.\n",
"\n",
"\n",
"2. Высокие вычислительные затраты \n",
" Аналитическое дифференцирование для сети с миллионами параметров:\n",
" - требует хранения гигантских символьных выражений;\n",
" - приводит к неприемлемому времени вычислений;\n",
" - потребляет чрезмерный объём памяти.\n",
"\n",
"\n",
"3. Сложность учёта нелинейностей \n",
" Каждый нейрон применяет нелинейную функцию активации (sigmoid, ReLU и др.). При аналитическом дифференцировании это порождает:\n",
" - вложенные производные сложных функций;\n",
" - необходимость многократного применения цепного правила;\n",
" - риск численной нестабильности (проблемы затухающих/взрывающихся градиентов).\n",
"\n",
"\n",
"4. Непрактичность обновления параметров \n",
" Даже если удалось вычислить все производные, их использование для обновления весов требует:\n",
" - формирования полного градиентного вектора;\n",
" - итеративных пересчётов при каждом изменении параметров;\n",
" - решения систем уравнений высокой размерности.\n",
"\n",
"Именно поэтому в обучении многослойных нейронных сетей (глубоком обучении, Deep Learning) повсеместно используют численное дифференцирование через обратное распространение ошибки (backpropagation), а не аналитические методы."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим суть алгоритма обратного распространения ошибки для многослойной нейронной сети общего вида.\n",
"\n",
"Пусть нейронная сеть состоит из $N$ слоёв. Каждый слой выполняет функциональное преобразование своих входных данных $G^{(i)}$ с использованием своих параметров (весов $W_i$). Кроме того, пусть $G^{(i)}$ не просто функция активации над взвешенными входными данными, а общее преобразование, включающее ещё и возможные архитектурные особенности (нормализация, дропаут и др.).\n",
"\n",
"Представим преобразование данных по слоям.\n",
"\n",
"Выход первого слоя: \n",
"$$\n",
"Y_1 = G^{(1)}_{W_1}(X),\n",
"$$ \n",
"где: \n",
"- $X$ — входные данные; \n",
"- $G^{(i)}$ — преобразование, выполняемое первым слоем; \n",
"- $W_1$ — параметры (пусть веса и смещение) первого слоя; \n",
"\n",
"Выход второго слоя: \n",
"$$\n",
"Y_2 = G^{(2)}_{W_2}(Y_1) = G^{(2)}_{W_2}\\bigl(G^{(1)}_{W_1}(X)\\bigr),\n",
"$$ \n",
"где: \n",
"- $G^{(2)}$ — преобразование второго слоя; \n",
"- $W_2$ — параметры второго слоя.\n",
"\n",
"Заметим, что $Y_2$ зависит: \n",
"- от входных данных $X$; \n",
"- от параметров первого слоя $W_1$; \n",
"- от параметров второго слоя $W_2$; \n",
"- от вида функций $G^{(1)}$ и $G^{(2)}$.\n",
"\n",
"Тогда выход всей сети из $N$ слоёв можно записать как композицию преобразований: \n",
"\n",
"$$\n",
"Y_{\\text{pred}} = G^{(N)}_{W_N}\\bigl(G^{(N-1)}_{W_{N-1}}\\bigl(\\cdots G^{(1)}_{W_1}(X)\\bigr)\\cdots\\bigr).\n",
"$$\n",
"\n",
"Очевидно, что замечания, данные выше для второго слоя, справедливы и даже масштабированы для выхода всей сети.\n",
"\n",
"Напомним, что задача обучения нашей нейронной сети заключается в том, чтобы для каждого её слоя нам необходимо подобрать параметры $\\{W_i\\}$, минимизирующие функцию потерь: \n",
"\n",
"$$\n",
"\\mathcal{L} = \\mathcal{L}(Y_{\\text{pred}}, Y_{\\text{true}}),\n",
"$$ \n",
"где: \n",
"- $Y_{\\text{pred}}$ — предсказание сети; \n",
"- $Y_{\\text{true}}$ — истинные значения (метки).\n",
"\n",
"И чтобы применить градиентный спуск, нужно для каждого слоя вычислить значение градиента $\\frac{\\partial \\mathcal{L}}{\\partial W_i}$ для формулы итеративного обновления параметров: \n",
"\n",
"$$\n",
"W_i^{[t+1]} \\leftarrow W_i^{[t]} - \\eta \\frac{\\partial \\mathcal{L}}{\\partial W_i},\n",
"$$\n",
"где $\\eta$ — скорость обучения."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Используя цепное правило дифференцирования (chain rule или правило частных производных), выразим градиенты по весам каждого слоя, начиная с последнего:\n",
"\n",
"- Для последнего слоя ($N$): \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial W_N} = \\frac{\\partial \\mathcal{L}}{\\partial G^{(N)}} \\cdot \\frac{\\partial G^{(N)}}{\\partial W_N}.\n",
" $$\n",
"\n",
"- Для предпоследнего слоя ($N-1$): \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial W_{N-1}} = \\frac{\\partial \\mathcal{L}}{\\partial G^{(N)}} \\cdot \\frac{\\partial G^{(N)}}{\\partial G^{(N-1)}} \\cdot \\frac{\\partial G^{(N-1)}}{\\partial W_{N-1}}.\n",
" $$\n",
"\n",
"- Для слоя ($N-2$): \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial W_{N-2}} = \\frac{\\partial \\mathcal{L}}{\\partial G^{(N)}} \\cdot \\frac{\\partial G^{(N)}}{\\partial G^{(N-1)}} \\cdot \\frac{\\partial G^{(N-1)}}{\\partial G^{(N-2)}} \\cdot \\frac{\\partial G^{(N-2)}}{\\partial W_{N-2}}.\n",
" $$ \n",
" \n",
"- **...** \n",
"\n",
"\n",
"- Для первого слоя ($1$): \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial W_1} = \\frac{\\partial \\mathcal{L}}{\\partial G^{(N)}} \\cdot \\frac{\\partial G^{(N)}}{\\partial G^{(N-1)}} \\cdot \\ldots \\cdot \\frac{\\partial G^{(2)}}{\\partial G^{(1)}} \\cdot \\frac{\\partial G^{(1)}}{\\partial W_1}.\n",
" $$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Ключевая идея обратного распространения ошибки (backpropagation) заключается в поэтапном накопленим градиентов с сохранением вычисленных ранее промежуточных результатов:\n",
"\n",
"- Выражение $\\dfrac{\\partial \\mathcal{L}}{\\partial G^{(N)}}$ вычисляется на первом шаге (для последнего слоя с номером $N$) — через непосредственную подстановку в него предсказание сети $Y_{\\text{pred}}$ и истинные значения $Y_{\\text{true}}$.\n",
"- Далее значение этого выражения, что очень важно, не пересчитывается заново, а используется при вычислении градиентов для более ранних слоёв — как минимум слоя с номером $N-1$.\n",
"- Аналогично, произведение $\\dfrac{\\partial \\mathcal{L}}{\\partial G^{(N)}} \\cdot \\dfrac{\\partial G^{(N)}}{\\partial G^{(N-1)}}$ сохраняется и применяется для слоя с номером $N-2$, и т. д.\n",
"\n",
"В результате такого обратного прохода мы получаем полный набор требуемых градиентов:\n",
"- $\\dfrac{\\partial \\mathcal{L}}{\\partial W_N}$ — по весам последнего слоя;\n",
"- $\\dfrac{\\partial \\mathcal{L}}{\\partial W_{N-1}}$ — по весам предпоследнего слоя;\n",
"- ...\n",
"- $\\dfrac{\\partial \\mathcal{L}}{\\partial W_1}$ — по весам первого слоя."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"После того как в ходе обратного распространения ошибки вычислены все необходимые градиенты $\\frac{\\partial \\mathcal{L}}{\\partial W_i}$ для каждого слоя, они непосредственно используются для обновления параметров сети.\n",
"\n",
"Описанный процесс образует итеративный цикл, который повторяется до достижения критерия остановки:\n",
"\n",
"1. Прямой проход: \n",
" - подача входных данных $X$ через сеть;\n",
" - вычисление выхода $Y_{\\text{pred}}$ и функции потерь $\\mathcal{L}$.\n",
"\n",
"2. Обратный проход (backpropagation): \n",
" - вычисление градиентов $\\frac{\\partial \\mathcal{L}}{\\partial W_i}$ для всех слоёв;\n",
" - сохранение промежуточных производных для эффективного пересчёта.\n",
"\n",
"3. Обновление параметров: \n",
" - применение формулы градиентного спуска для всех $W_i$;\n",
" - корректировка весов и смещений сети.\n",
"\n",
"4. Проверка условия остановки: \n",
" - достигнута ли заданная точность ($\\mathcal{L} \\leq \\varepsilon$)?\n",
" - исчерпано ли максимальное число итераций?\n",
" - стабилизировалась ли ошибка на валидационной выборке?\n",
"\n",
"Если критерий остановки не выполнен, цикл повторяется: новый прямой проход с обновлёнными весами, затем обратное распространение и очередное обновление.\n",
"\n",
"Обучение завершается при выполнении одного из условий (критерия остановки):\n",
"- функция потерь $\\mathcal{L}$ достигла заранее заданного порога $\\varepsilon$;\n",
"- число итераций (эпох) достигло максимального значения;\n",
"- ошибка на валидационной выборке перестаёт уменьшаться (признак переобучения);\n",
"- изменение $\\mathcal{L}$ между итерациями становится пренебрежимо малым."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Преимущества и особенности алгоритма обратного распространения ошибки:\n",
"\n",
"1. Эффективность вычислений.\n",
" Избегается повторное вычисление одних и тех же производных. Сложность становится линейной относительно числа параметров (вместо экспоненциальной при прямом аналитическом дифференцировании).\n",
"\n",
"2. Экономия памяти. \n",
" Промежуточные градиенты хранятся только для текущего шага обратного прохода. Не требуется хранить гигантские символьные выражения.\n",
"\n",
"3. Масштабируемость. \n",
" Алгоритм работает одинаково эффективно для сетей любой глубины. Легко адаптируется к различным архитектурам (свёрточные, рекуррентные и др.).\n",
"\n",
"4. Физическая интерпретация. \n",
" Ошибка «распространяется обратно» от выхода к входу, последовательно уточняя вклад каждого слоя. Каждый шаг использует результаты предыдущего, что напоминает волновой процесс.\n",
"\n",
"Алгоритм обратного распространения ошибки подходит для сетей любой архитектуры: от простейших однонейронных моделей (подобных той, которую мы рассматривали выше) до глубоких многослойных сетей без принципиальных изменений в процедуре обучения. Он универсален для различных типов слоёв (полносвязных, свёрточных, рекуррентных и др.), совместим с разными функциями активации и функциями потерь."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим пошагово пример обратного прохода для задачи логического «И».\n",
"\n",
"Предварительно нам нужно задать функции частных производных использованных функции потерь MSE (производная по выходному результату сети) и сигмоиды как функции активации (производная по взвешенным входным данным):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def mse_derivative(y_pred, y_true):\n",
" return 2 * (y_pred - y_true) * 1\n",
"\n",
"def sigmoid_derivative(s):\n",
" z = sigmoid(s)\n",
" return z * (1 - z)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В ячейке ниже задайте функции частных производных функций потерь и функций активации, которые вы вводили:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Ваш код здесь"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Чтобы ускорить сходимость и улучшить стабильность обучения, модифицируем стандартную формулу градиентного спуска — введём понятие импульса (momentum).\n",
"\n",
"Идея модификации в том, что импульс учитывает историю изменений градиентов: вместо того чтобы корректировать веса только по текущему градиенту, мы добавляем «память» о предыдущих направлениях движения в пространстве параметров. Это позволяет быстрее преодолевать плоские участки функции потерь, сглаживать колебания при осциллирующих градиентах и ускорять сходимость в овражных зонах ландшафта ошибки.\n",
"\n",
"Введём вспомогательную переменную — вектор скорости $v_i$ для параметров слоя $W_i$, который аккумулирует информацию о предыдущих градиентах.\n",
"\n",
"Обновление параметров при этом происходит в два этапа:\n",
"\n",
"1. Расчёт новой скорости (с учётом импульса): \n",
" \n",
" $$\n",
" v_i^{[t]} = \\beta \\cdot v_i^{[t-1]} + \\eta \\cdot \\frac{\\partial \\mathcal{L}}{\\partial W_i^{[t]}},\n",
" $$ \n",
" где: \n",
" - $v_i^{[t]}$ — скорость (накопительный вектор) для параметров слоя $W_i$ на шаге $t$; \n",
" - $v_i^{[t-1]}$ — значение скорости на предыдущем шаге (инициализируется нулём); \n",
" - $\\beta \\in [0, 1)$ — коэффициент импульса (обычно $0.9$–$0.99$); \n",
" - $\\eta$ — скорость обучения (learning rate); \n",
" - $\\frac{\\partial \\mathcal{L}}{\\partial W_i^{[t]}}$ — градиент функции потерь по параметрам $W_i$ на текущем шаге.\n",
"\n",
"\n",
"2. Обновление весов с использованием накопленной скорости: \n",
" \n",
" $$\n",
" W_i^{[t+1]} = W_i^{[t]} - v_i^{[t]},\n",
" $$ \n",
" где $W_i^{[t]}$ — текущие значения параметров слоя.\n",
"\n",
"Импульс (momentum) — простой, но мощный способ улучшить градиентный спуск. Он добавляет «физику движения» в оптимизацию: веса не просто шагают по градиенту, а разгоняются в направлении устойчивого спуска, что ускоряет обучение и повышает устойчивость.\n",
"\n",
"*Примечание*. Это только одна из возможных реализаций импульса. Прикладное удобство данного варианта в том, что при $\\beta = 0$ получается классическое обновление параметров."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Напомним исходные веса и смещение в нашей задаче:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print('Веса:\\n', w)\n",
"print('\\nСмещение:', b)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Задайте скорость обучение и коэффициент импульса (согласно рекомендациям для каждого из этих гиперпараметров):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"learning_rate = # Ваш код здесь\n",
"momentum = # Ваш код здесь"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Инициализируем нулями значения скоростей:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"velocity_w = np.zeros_like(w)\n",
"velocity_b = 0.0 "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим пошагово реализацию кода обратного распространения ошибки.\n",
"\n",
"<a id=\"loop_and\"></a>Первый шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dL_dz = mse_derivative(z, y_and_data)\n",
"print(dL_dz)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вычислены значения производной функции потерь (MSE) по выходу сигмоиды $z$. \n",
"\n",
"Математическая запись:\n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial z} = \\frac{\\partial}{\\partial z}\\left[(z - y)^2\\right] = 2 \\cdot (z - y),\n",
" $$ \n",
" где: \n",
" - $z$ (`z`) — выход сигмоиды (предсказание модели); \n",
" - $y$ (`y_and_data`) — истинные значения (целевые метки). \n",
"\n",
"Второй шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dz_ds = sigmoid_derivative(s)\n",
"print(dz_ds)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вычислены значения производной сигмоиды по её входу $s$ (взвешенной сумме). \n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial z}{\\partial s} = \\sigma'(s) = \\sigma(s) \\cdot (1 - \\sigma(s)) = z \\cdot (1 - z),\n",
" $$ \n",
" где: \n",
" - $\\sigma(s)$ — функция сигмоиды; \n",
" - $s$ (`z`) — взвешенная сумма входных данных (до применения активации); \n",
" - $z = \\sigma(s)$ — выход сигмоиды. \n",
" \n",
"Третий шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dL_ds = dL_dz * dz_ds\n",
"print(dL_ds)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вычислены значения функции потерь по взвешенной сумме $s$ через правило частных производных. \n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial s} = \\frac{\\partial \\mathcal{L}}{\\partial z} \\cdot \\frac{\\partial z}{\\partial s},\n",
" $$ \n",
"\n",
"Таким образом, объединены два предыдущих градиента: \n",
" - как ошибка зависит от выхода $z$ (`dL_dz`); \n",
" - как выход $z$ зависит от входа $s$ (`dz_ds`). \n",
" \n",
"Последний шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dL_dw = np.dot(X_data.T, dL_ds)\n",
"dL_db = np.sum(dL_ds)\n",
"\n",
"print(dL_dw)\n",
"print(dL_db)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В первой строке вычисляются значения градиента функции потерь по весовым коэффициентам $W$ (вектор/матрица $\\frac{\\partial \\mathcal{L}}{\\partial W}$).\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial W} = X^\\mathsf{T} \\cdot \\frac{\\partial \\mathcal{L}}{\\partial s},\n",
" $$ \n",
" где: \n",
" - $X$ (`X_data`) — матрица входных данных размера $(n, m)$, где $n$ — число объектов, $m$ — число признаков; \n",
" - $X^\\mathsf{T}$ — транспонированная матрица (размер $(m, n)$); \n",
" - $\\frac{\\partial \\mathcal{L}}{\\partial s}$ (`dL_ds`) — градиент по взвешенным суммам размера $(n, 1)$; \n",
" - результат — матрица градиентов размера $(m, 1)$.\n",
"\n",
"Операция `np.dot` выполняет матричное умножение: каждый элемент результата — скалярное произведение строки $X^\\mathsf{T}$ (признака) и столбца `dL_ds` (градиентов по объектам). В итоге мы получаем вектор/матрицу градиентов, показывающих, как изменение каждого веса влияет на ошибку.\n",
"\n",
"Во второй строке вычисляются значения градиента функции потерь по смещению $b$ (скаляр или вектор $\\frac{\\partial \\mathcal{L}}{\\partial b}$).\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial b} = \\sum_{i=1}^{n} \\frac{\\partial \\mathcal{L}}{\\partial s_i},\n",
" $$ \n",
" где: \n",
" - $\\frac{\\partial \\mathcal{L}}{\\partial s_i}$ — градиент ошибки по взвешенной сумме для $i$-го объекта (`dL_ds[i]`); \n",
" - суммирование идёт по всем $n$ объектам выборки.\n",
"\n",
"Смещение $b$ при прямом проходе одинаково применяется ко всем объектам, поэтому его градиент — это сумма градиентов `dL_ds` по всем примерам. Операция суммирования `np.sum` агрегирует градиенты: поскольку `dL_ds` имеет размер $(n, 1)$, результат — скаляр. Физический смысл: показывается, насколько нужно изменить $b$, чтобы уменьшить ошибку в среднем по выборке.\n",
"\n",
"С помощью вычисленных градиентов `dL_dw` и `dL_db` обновим скорости для весов и для смещения соответственно:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"velocity_w = momentum * velocity_w + learning_rate * dL_dw\n",
"velocity_b = momentum * velocity_b + learning_rate * dL_db"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Обновим веса и смещение непосредственно:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"w -= velocity_w\n",
"b -= velocity_b\n",
"\n",
"print('Веса:\\n', w)\n",
"print('\\nСмещение:', b)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Проверим, как с обновлёнными весами изменятся значение функции потерь и метрики accuracy. Произведём прямой проход:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"s = X_data.dot(w) + b\n",
"z = sigmoid(s)\n",
"model_answer_interpretation = (z >= 0.5).astype('int')\n",
"\n",
"print(model_answer_interpretation)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Среднеквадратичная ошибка:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"mse(z, y_and_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Доля правильно классифицированных примеров:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"accuracy(model_answer_interpretation, y_and_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Сделайте несколько проходов (итераций) от [первого шага обратного распространения ошибки](#loop_and) до данной ячейки. Убедитесь, что ошибка сети понижается, а качество классификации растёт."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. Решение задачи логического «И»<a id=\"p_3\"></a>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Сведём весь код, представленный выше для непосредственного решения задачи логического «И». Вы можете использовать либо его, либо модифицированный на его основе — например, реализовать функцию обучения нейроннойсети и проверки её работы.\n",
"\n",
"Перед обучением сети задайте (или передайте параметрами в реализованную функцию):\n",
" - данные для обучения (входные и выходные);\n",
" - функцию потерь и функцию активации, а также их производные (можете использовать реализации для MSE и сигмоиды);\n",
" - начальные значения весов и смещения;\n",
" - количество итераций в процессе обучения (иначе говоря, эпох — т.е. сколько раз в процессе обучения нейронная сеть будет использовать все имеющиеся для этого данные) (желательно больше 5);\n",
" - скорость обучение и коэффициент импульса."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"X = X_data\n",
"y_true = y_and_data\n",
"\n",
"loss_function = # Ваш код здесь\n",
"loss_derivative = # Ваш код здесь\n",
"activate_function = # Ваш код здесь\n",
"activate_derivative = # Ваш код здесь\n",
"\n",
"w = # Ваш код здесь\n",
"b = # Ваш код здесь\n",
"\n",
"epochs = # Ваш код здесь\n",
"\n",
"# Гиперпараметры\n",
"learning_rate = # Ваш код здесь\n",
"momentum = # Ваш код здесь"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Обучение нейронной сети для решения задачи логического «И»:**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Инициализация скоростей для импульса\n",
"velocity_w = np.zeros_like(w)\n",
"velocity_b = 0.0\n",
"\n",
"# Список для хранения истории потерь\n",
"loss_history = []\n",
"\n",
"for epoch in range(epochs):\n",
" \n",
" # Прямой проход\n",
" s = np.dot(X, w) + b\n",
" z = activate_function(s)\n",
" model_answer_interpretation = (z >= 0.5).astype('int')\n",
" \n",
" # Вычисляем ошибку\n",
" loss = loss_function(z, y_true)\n",
" loss_history.append(loss)\n",
" \n",
" # Обратное распространение\n",
" dL_dz = loss_derivative(z, y_true)\n",
" dz_ds = activate_derivative(s)\n",
" dL_ds = dL_dz * dz_ds\n",
" \n",
" # Градиенты\n",
" dL_dw = np.dot(X.T, dL_ds)\n",
" dL_db = np.sum(dL_ds)\n",
" \n",
" # Обновление скоростей\n",
" velocity_w = momentum * velocity_w + learning_rate * dL_dw\n",
" velocity_b = momentum * velocity_b + learning_rate * dL_db\n",
" \n",
" # Обновление параметров\n",
" w -= velocity_w\n",
" b -= velocity_b\n",
" \n",
" # Вывод каждые 5 эпох\n",
" if (epoch + 1) % 5 == 0:\n",
" \n",
" clear_output(True)\n",
" plt.plot(range(1, epoch+2), loss_history, label='Loss')\n",
" plt.title(f'Epoch: {epoch + 1}, Loss: {loss:.6f}') \n",
" plt.grid(True, alpha=0.3)\n",
" plt.legend(loc='best')\n",
" plt.show()\n",
" \n",
" print('Test:')\n",
" for i in range(len(X)):\n",
" print(f'Input: {X[i]}, Output: {np.round(z[i], 2)}, Prediction: {model_answer_interpretation[i]}, True: {y_true[i]}')\n",
"\n",
" print(f'Accuracy: {accuracy(model_answer_interpretation, y_true):.2f}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Убедитесь, что сеть обучена хорошо — т.е. у неё высокое качество классификации, а в процессе обучения ошибка стаюильно снижалась:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"assert accuracy(model_answer_interpretation, y_and_data) == 1\n",
"assert np.mean(loss_history[:5]) > np.mean(loss_history[-5:])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Поэкспериментируйте в процессе подбора параметров. Сделайте несколько вариантов с разными параметрами и выберите лучшие по качеству обучения модели, по её ошибке и по количеству эпох (т.е. как быстро она обучилась с такими параметрами).\n",
"\n",
"Отдельно проведите исследование, как на обучение сети влияют значения скорости обучение (`learning_rate`) и коэффициента импульса (`momentum`)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. Решение задачи логического «ИЛИ»<a id=\"p_4\"></a>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"По образцу решения задачи логического «И» решите с помощью нейронной сети, состоящей из одного нейрона, задачу логического «ИЛИ» логическое сложение). Напомним, павило работы операции:\n",
" - выход равен `0` олько если оба входа равны `0`;\n",
" - выход равен `1` если хотя бы один из входов равен `1`.\n",
"\n",
"Таблица истинности операции «ИЛИ»:\n",
"\n",
"$$\n",
"\\begin{array}{c|c|c}\n",
"x_1 & x_2 & y \\\\\n",
"\\hline\n",
"0 & 0 & 0 \\\\\n",
"0 & 1 & 1 \\\\\n",
"1 & 0 & 1 \\\\\n",
"1 & 1 & 1 \\\\\n",
"\\end{array}\n",
"$$\n",
"\n",
"Результат в массиве `y_or_data`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"y_or_data = np.array([0, 1, 1, 1]).reshape(-1, 1)\n",
"print(y_or_data)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Ваш код здесь"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Ваш код здесь"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Проверьте обученную вами сеть:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"assert accuracy(model_answer_interpretation, y_or_data) == 1\n",
"assert np.mean(loss_history[:5]) > np.mean(loss_history[-5:])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Поэкспериментируйте в процессе подбора параметров. Сделайте несколько вариантов с разными параметрами и выберите лучшие по качеству обучения модели, по её ошибке и по количеству эпох (т.е. как быстро она обучилась с такими параметрами).\n",
"\n",
"Отдельно проведите исследование, как на обучение сети влияют значения скорости обучение (`learning_rate`) и коэффициента импульса (`momentum`)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. Решение задачи логического «исключающего ИЛИ»<a id=\"p_5\"></a>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Правило работы операции «исключающего ИЛИ» (сумма по модулю 2):\n",
" - выход равен `0` если входы одинаковы (оба `0` или оба `1`);\n",
" - выход равен `1` если входы различны (один `0`, другой `1`).\n",
"\n",
"Таблица истинности операции «исключающего ИЛИ»:\n",
"\n",
"$$\n",
"\\begin{array}{c|c|c}\n",
"x_1 & x_2 & y \\\\\n",
"\\hline\n",
"0 & 0 & 0 \\\\\n",
"0 & 1 & 1 \\\\\n",
"1 & 0 & 1 \\\\\n",
"1 & 1 & 0 \\\\\n",
"\\end{array}\n",
"$$\n",
"\n",
"Результат в массиве `y_xor_data`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"y_xor_data = np.array([0, 1, 1, 0]).reshape(-1, 1)\n",
"print(y_xor_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим проблему «исключающего ИЛИ» с помощью графического представления, сопоставив её с другими базовыми логическими операциями.\n",
"\n",
"Операции «И» и «ИЛИ» могут быть решены одним линейным классификатором — то есть нейронной сетью с единственным нейроном. Это возможно, поскольку данные операции являются линейно разделимыми: их входные комбинации можно разделить на классы с помощью одной прямой (гиперплоскости в двумерном пространстве):\n",
"\n",
"![Операции И и ИЛИ](https://sun9-14.userapi.com/s/v1/ig2/qqdYL6RKIEnKOVTQX57Zh4dx_oJ9qL8LNABXJKc4eHWY34C-dsGXyi6-45BTy93H4xzf90snp3QV8QwGwVrlJVBU.jpg?quality=95&as=32x18,48x27,72x41,108x61,160x90,240x135,327x184&from=bu&cs=327x0)\n",
"\n",
"В отличие от них, операция «исключающего ИЛИ» не является линейно разделимой. Для её реализации требуется как минимум два линейных классификатора (или нейронная сеть с одним скрытым слоем, содержащим два нейрона). Такая архитектура позволяет:\n",
" - сначала выделить промежуточные признаки (например, области «выше» и «ниже» двух разделяющих прямых);\n",
" - затем скомбинировать их для получения итогового результата «исключающего ИЛИ».\n",
"\n",
"![Операция исключающего ИЛИ](https://sun9-16.userapi.com/s/v1/ig2/55_GHsfUuJqQ8t2pUupUc7FxS-IlhKzaXoy1yhm95eHK8HsT9qW8hv13N67dvFKg6D_e1gaNBSgTPnvtRSd5XM4h.jpg?quality=95&as=32x18,48x26,72x40,108x59,160x88,240x132,298x164&from=bu&cs=298x0)\n",
"\n",
"Таким образом, задача «исключающего ИЛИ» демонстрирует необходимость нелинейного разделения и служит классическим примером, показывающим ограничения однонейронных моделей и пользу скрытых слоёв."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим, как данные проходят через нейронную сеть при решении задачи «исключающего ИЛИ» (прямой проход). Для наглядности и удобства отслеживания размерностей зададим в скрытом слое 3 нейрона.\n",
"\n",
"Задаём веса связей от входного слоя к скрытому и смещение для каждого из нейронов скрытого слоя (веса и смещения входного слоя):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"n_hidden_neurons = 3\n",
"\n",
"w1 = np.random.uniform(low=-.05, high=.05, size=(2, n_hidden_neurons))\n",
"b1 = np.random.uniform(low=-.05, high=.05, size=(1, n_hidden_neurons))\n",
"\n",
"print('Веса входного слоя:\\n', w1)\n",
"print('\\nСмещения входного слоя:\\n', b1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Для завершения архитектуры сети зададим параметры веса связей от скрытого слоя к выходу и смещение (выходного слоя):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"w2 = np.random.uniform(low=-.05, high=.05, size=(n_hidden_neurons, 1))\n",
"b2 = np.random.uniform(low=-.05, high=.05, size=(1, 1))\n",
"\n",
"print('Веса выходного слоя:\\n', w2)\n",
"print('\\nСмещение выходного слоя:\\n', b2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Умножаем входные данные размера $(n, 2)$ на веса входного слоя размера $(2, h)$ (где $h$ — количество нейронов в скрытом слое) и прибавляем к каждому столбцу результата соответствующее смещение для каждого нейрона скрытого слоя.\n",
"\n",
"Это формирует матрицу со взвешенными суммами (`s1`) размера $(n, h)$, которая далее будет передана через функцию активации:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"s1 = np.dot(X_data, w1) + b1\n",
"print(s1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Полученный результат `s1` пропускаем через функцию активации скрытого слоя (пусть это будет сигмоида):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"z1 = sigmoid(s1)\n",
"print(z1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Умножаем выходные данные скрытого слоя `z1` размера $(n, h)$ на веса выходного слоя $(h, 1)$ и прибавляем к полученному столбцу размера $(n, 1)$ смещение для выходного нейрона:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"s2 = np.dot(z1, w2) + b2\n",
"print(s2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Пропускаем `s2` через функцию активации и получаем выход нейронной сети `z2`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"z2 = sigmoid(s2)\n",
"print(z2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Значение функции потерь:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"mse(z2, y_xor_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Метрика accuracy:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"model_answer_interpretation = (z2 >= 0.5).astype('int')\n",
"print(model_answer_interpretation)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"accuracy(model_answer_interpretation, y_xor_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В прямом прохое мы убедились, что можем задавать любое количество нейронов в скрытом слое для решения нашей задачи (и ей подобных), и получили адекватный результат.\n",
"\n",
"Перейдём к обучению нейронной сети через обратное распространение ошибки. Рассмотрим обратный проход.\n",
"\n",
"В ходе прямого прохода мы продемонстрировали ключевую гибкость архитектуры:\n",
"- можно задавать любое количество нейронов в скрытом слое — это позволяет адаптировать модель под сложность задачи (в том числе для «исключающего ИЛИ» и аналогичных);\n",
"- даже при произвольном числе нейронов сеть выдаёт адекватный промежуточный результат на выходе.\n",
"\n",
"Теперь переходим к следующему этапу — обучению нейронной сети. Основной механизм обучения — также обратное распространение ошибки (backpropagation). Сосредоточимся на обратном проходе и покажем:\n",
"1. Как вычисляются градиенты функции потерь по параметрам сети.\n",
"2. Как эти градиенты используются для корректировки весов и смещений.\n",
"3. Каким образом ошибка «распространяется» от выходного слоя к входному."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Напомним исходные веса и смещение в нашей задаче:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print('Веса входного слоя:\\n', w1)\n",
"print('\\nСмещения входного слоя:\\n', b1)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print('Веса выходного слоя:\\n', w2)\n",
"print('\\nСмещение выходного слоя:\\n', b2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Задайте скорость обучение и коэффициент импульса (согласно рекомендациям для каждого из этих гиперпараметров):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"learning_rate = # Выш код здесь\n",
"momentum = # Выш код здесь"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Инициализируем нулями значения скоростей:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"velocity_w1 = np.zeros_like(w1)\n",
"velocity_b1 = np.zeros_like(b1)\n",
"velocity_w2 = np.zeros_like(w2)\n",
"velocity_b2 = np.zeros_like(b2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим пошагово реализацию кода обратного распространения ошибки.\n",
"\n",
"<a id=\"loop_xor\"></a>Первый шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dL_dz2 = mse_derivative(z2, y_xor_data)\n",
"print(dL_dz2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вычисляются значения производной функции потерь (MSE) по выходу выходного нейрона $z_2$.\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial z_2} = \\frac{\\partial}{\\partial z_2}\\left[(z_2 - y)^2\\right] = 2 \\cdot (z_2 - y),\n",
" $$ \n",
" где: \n",
" - $z_2$ (`z2`) — выход выходного нейрона (предсказание сети); \n",
" - $y$ (`y_xor_data`) — истинные значения (целевые метки для XOR).\n",
"\n",
"Размерность: \n",
" - `z2`: $(n, 1)$ — $n$ объектов, 1 выходной нейрон; \n",
" - `y_xor_data`: $(n, 1)$; \n",
" - результат `dL_dz2`: $(n, 1)$.\n",
" \n",
"Второй шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dz2_ds2 = sigmoid_derivative(s2)\n",
"print(dz2_ds2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вычисляются значения производной сигмоиды по её входу $s_2$ (взвешенной сумме на выходном нейроне).\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial z_2}{\\partial s_2} = \\sigma'(s_2) = \\sigma(s_2) \\cdot (1 - \\sigma(s_2)) = z_2 \\cdot (1 - z_2),\n",
" $$ \n",
" где $\\sigma(s_2)$ — функция сигмоиды, $s_2$ (`s2`) — взвешенная сумма на выходном нейроне.\n",
"\n",
"Размерность: \n",
" - `s2`: $(n, 1)$ — $n$ объектов, 1 нейрон; \n",
" - результат `dz2_ds2`: $(n, 1)$.\n",
" \n",
"Третий шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dL_ds2 = dL_dz2 * dz2_ds2\n",
"print(dL_ds2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вычисляются значения производной функции потерь по взвешенной сумме $s_2$ через правило частных производных.\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial s_2} = \\frac{\\partial \\mathcal{L}}{\\partial z_2} \\cdot \\frac{\\partial z_2}{\\partial s_2},\n",
" $$ \n",
"\n",
"Размерность (поэлементное умножение): \n",
" - `dL_dz2`: $(n, 1)$; \n",
" - `dz2_ds2`: $(n, 1)$; \n",
" - результат `dL_ds2`: $(n, 1)$.\n",
"\n",
"Таким образом, объединены два предыдущих градиента: \n",
" - как ошибка зависит от выхода $z_2$ (`dL_dz2`); \n",
" - как выход $z_2$ зависит от входа $s_2$ (`dz2_ds2`). \n",
"\n",
"Четвёртый шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dL_dw2 = np.dot(z1.T, dL_ds2)\n",
"dL_db2 = np.sum(dL_ds2, axis=0, keepdims=True)\n",
"\n",
"# Проверки размерностей: \n",
"assert dL_dw2.shape == w2.shape\n",
"assert dL_db2.shape == b2.shape\n",
"\n",
"print(dL_dw2)\n",
"print(dL_db2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В первой строке вычисляются значения градиента функции потерь по весам выходного слоя $W_2$: $\\frac{\\partial \\mathcal{L}}{\\partial W_2}$.\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial W_2} = z_1^\\mathsf{T} \\cdot \\frac{\\partial \\mathcal{L}}{\\partial s_2},\n",
" $$ \n",
" где: \n",
" - $z_1$ (`z1`) — активации скрытого слоя (выход скрытого слоя после активации); \n",
" - $\\frac{\\partial \\mathcal{L}}{\\partial s_2}$ (`dL_ds2`) — градиент потерь по взвешенной сумме на выходном нейроне; \n",
" - $\\cdot$ — матричное умножение.\n",
"\n",
"Размерность: \n",
" - `z1`: $(n, h)$ — $n$ объектов, $h$ нейронов в скрытом слое; \n",
" - `dL_ds2`: $(n, 1)$ — градиент по выходному нейрону для $n$ объектов; \n",
" - результат `dL_dw2`: $(h, 1)$ — соответствует размеру `w2` $(h, 1)$.\n",
"\n",
"В итоге получаем матрицу градиентов, показывающую, как изменение каждого веса $W_2$ влияет на ошибку. Её мы передадим в формулу обновления весов выходного слоя.\n",
"\n",
"Во второй строке вычисляются значения градиента функции потерь по смещениям выходного слоя $b_2$: $\\frac{\\partial \\mathcal{L}}{\\partial b_2}$.\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial b_2} = \\sum_{i=1}^{n} \\frac{\\partial \\mathcal{L}}{\\partial s_{2i}},\n",
" $$ \n",
" где $s_{2i}$ — взвешенная сумма для $i$-го объекта.\n",
"\n",
"Размерность: \n",
" - `dL_ds2`: $(n, 1)$; \n",
" - результат `dL_db2`: $(1, 1)$.\n",
"\n",
"Смещение $b_2$ одинаково применяется ко всем объектам, поэтому его градиент — это сумма градиентов `dL_ds2` по всем $n$ примерам. Параметр `axis=0` задаёт суммирование по оси объектов ($n$), `keepdims=True` сохраняет размерность $(1, 1)$, чтобы соответствовать форме `b2` $(1, 1)$. Результат мы передадим в формулу обновления смещения выходного слоя.\n",
"\n",
"Пятый шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dL_dz1 = np.dot(dL_ds2, w2.T)\n",
"print(dL_dz1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вычисляются значения градиента функции потерь по выходам скрытого слоя $z_1$: $\\frac{\\partial \\mathcal{L}}{\\partial z_1}$.\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial z_1} = \\frac{\\partial \\mathcal{L}}{\\partial s_2} \\cdot W_2^\\mathsf{T},\n",
" $$ \n",
" где: \n",
" - $\\frac{\\partial \\mathcal{L}}{\\partial s_2}$ (`dL_ds2`) — градиент по взвешенной сумме выходного нейрона; \n",
" - $W_2$ (`w2`) — веса между скрытым и выходным слоем; \n",
" - $\\cdot$ — матричное умножение.\n",
"\n",
"Размерность: \n",
" - `dL_ds2`: $(n, 1)$ — градиент для $n$ объектов по выходному нейрону; \n",
" - `w2.T`: $(1, h)$ — транспонированные веса выходного слоя (1 выход × $h$ скрытых нейрона); \n",
" - результат `dL_dz1`: $(n, h)$ — градиент по выходам 3 нейронов скрытого слоя для $n$ объектов.\n",
" \n",
"Шестой шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dz1_ds1 = sigmoid_derivative(s1)\n",
"print(dz1_ds1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вычисляются значения производной сигмоиды по взвешенным суммам скрытого слоя: $\\frac{\\partial z_1}{\\partial s_1}$.\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial z_1}{\\partial s_1} = \\sigma'(s_1) = z_1 \\cdot (1 - z_1),\n",
" $$ \n",
" где $\\sigma(s_1)$ — функция сигмоиды, $s_1$ (`s1`) — взвешенные суммы на нейронах скрытого слоя.\n",
"\n",
"Размерность: \n",
" - `s1`: $(n, h)$ — $n$ объектов, $h$ нейронов скрытого слоя; \n",
" - результат `dz1_ds1`: $(n, h)$.\n",
" \n",
"Седьмой шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dL_ds1 = dL_dz1 * dz1_ds1\n",
"print(dL_ds1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вычисляются значения градиаента функции потерь по взвешенным суммам скрытого слоя через правило частных производных: $\\frac{\\partial \\mathcal{L}}{\\partial s_1}$.\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial s_1} = \\frac{\\partial \\mathcal{L}}{\\partial z_1} \\cdot \\frac{\\partial z_1}{\\partial s_1},\n",
" $$ \n",
"\n",
"Размерность (поэлементное умножение): \n",
" - `dL_dz1`: $(n, h)$; \n",
" - `dz1_ds1`: $(n, h)$; \n",
" - результат `dL_ds1`: $(n, h)$.\n",
"\n",
"Таким образом, объединены два предыдущих градиента:\n",
" - как ошибка зависит от выходов скрытого слоя (`dL_dz1`); \n",
" - как выходы зависят от взвешенных сумм (`dz1_ds1`). \n",
"\n",
"Последний шаг:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dL_dw1 = np.dot(X_data.T, dL_ds1)\n",
"dL_db1 = np.sum(dL_ds1, axis=0, keepdims=True)\n",
"\n",
"# Проверки размерностей:\n",
"assert w1.shape == dL_dw1.shape\n",
"assert b1.shape == dL_db1.shape\n",
"\n",
"print(dL_dw1)\n",
"print(dL_db1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В первой строке вычисляются значения градиента функции потерь по весам входного слоя: $\\frac{\\partial \\mathcal{L}}{\\partial W_1}$.\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial W_1} = X^\\mathsf{T} \\cdot \\frac{\\partial \\mathcal{L}}{\\partial s_1},\n",
" $$ \n",
" где $X$ (`X_data`) — входные данные.\n",
"\n",
"Размерность: \n",
" - `X_data.T`: $(2, n)$ — транспонированные входные данные (2 признака × $n$ объектов); \n",
" - `dL_ds1`: $(n, h)$ — градиенты по взвешенным суммам скрытого слоя; \n",
" - результат `dL_dw1`: $(2, h)$ — соответствует размеру `w1` $(2, h)$.\n",
"\n",
"В итоге получаем матрицу градиентов обновления весов входного слоя.\n",
"\n",
"Во второй строке вычисляются значения градиента функции потерь по смещениям входного слоя: $\\frac{\\partial \\mathcal{L}}{\\partial b_1}$.\n",
"\n",
"Математическая запись: \n",
" $$\n",
" \\frac{\\partial \\mathcal{L}}{\\partial b_1} = \\sum_{i=1}^{n} \\frac{\\partial \\mathcal{L}}{\\partial s_{1i}},\n",
" $$ \n",
" где $s_{1i}$ — взвешенная сумма для $i$-го объекта.\n",
"\n",
"Размерность: \n",
" - `dL_ds1`: $(n, h)$; \n",
" - результат `dL_db1`: $(1, h)$ (благодаря `keepdims=True`).\n",
"\n",
"В итоге получаем матрицу градиентов обновления смещений входного слоя."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В результате обратного прохода все требуемые градиаенты получены.\n",
"\n",
"Обновим скорости для всех соответствующих весов и смещений:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"velocity_w1 = momentum * velocity_w1 + learning_rate * dL_dw1\n",
"velocity_b1 = momentum * velocity_b1 + learning_rate * dL_db1\n",
"velocity_w2 = momentum * velocity_w2 + learning_rate * dL_dw2\n",
"velocity_b2 = momentum * velocity_b2 + learning_rate * dL_db2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Обновим сами веса и смещения:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"w1 -= velocity_w1\n",
"b1 -= velocity_b1\n",
"w2 -= velocity_w2\n",
"b2 -= velocity_b2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Произведём с обновлёнными весами прямой проход данных через нейронную сеть и получим значение среднеквадратичной ошибки:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"s1 = np.dot(X_data, w1) + b1\n",
"z1 = sigmoid(s1)\n",
"s2 = np.dot(z1, w2) + b2\n",
"z2 = sigmoid(s2)\n",
"\n",
"mse(z2, y_xor_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Доля правильно классифицированных примеров:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"model_answer_interpretation = (z2 >= 0.5).astype('int')\n",
"accuracy(model_answer_interpretation, y_xor_data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Сделайте несколько проходов (итераций) от [первого шага обратного распространения ошибки](#loop_xor) до данной ячейки. Убедитесь, что ошибка сети понижается, а качество классификации растёт."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Сведём весь код решения задачи логического «исключающего ИЛИ». Вы можете опираться на него либо реализовать своё решение.\n",
"\n",
"Перед обучением сети задайте (или передайте параметрами в реализованную функцию):\n",
" - данные для обучения (входные и выходные);\n",
" - функцию потерь и её производные (можете использовать реализации для MSE);\n",
" - функции активации для неронов входного и выходного слоёв, а также их производные (можете выбрать либо одну и ту же функцию, либо разные); \n",
" - количество нейронов в скрытом слое (можно оставить 2)\n",
" - начальные значения весов и смещения (приведён пример задания через нормальное распределение) (главное, соблюдать размерность);\n",
" - количество эпох или итераций в процессе обучения (желательно больше 5);\n",
" - скорость обучение и коэффициент импульса."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"X = X_data\n",
"y_true = y_xor_data\n",
"\n",
"loss_function = # Ваш код здесь\n",
"loss_derivative = # Ваш код здесь\n",
"activate_function_1 = # Ваш код здесь\n",
"activate_derivative_1 = # Ваш код здесь\n",
"activate_function_2 = # Ваш код здесь\n",
"activate_derivative_2 = # Ваш код здесь\n",
"\n",
"n_hidden_neurons = # Ваш код здесь\n",
"\n",
"# Перебор seed для инициализации параметров\n",
"np.random.seed(seed=42)\n",
"\n",
"# Входной слой\n",
"w1 = np.random.randn(2, n_hidden_neurons) * 0.5\n",
"b1 = np.random.randn(1, n_hidden_neurons) * 0.5\n",
"\n",
"# Выходной слой\n",
"w2 = np.random.randn(n_hidden_neurons, 1) * 0.5\n",
"b2 = np.random.randn(1, 1) * 0.5\n",
"\n",
"epochs = # Ваш код здесь\n",
"\n",
"# Гиперпараметры\n",
"learning_rate = # Ваш код здесь\n",
"momentum_beta = # Ваш код здесь"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Обучение нейронной сети для решения задачи «исключающего ИЛИ»:**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"# Инициализация скоростей для импульса\n",
"velocity_w1 = np.zeros_like(w1)\n",
"velocity_b1 = np.zeros_like(b1)\n",
"velocity_w2 = np.zeros_like(w2)\n",
"velocity_b2 = np.zeros_like(b2)\n",
"\n",
"# Список для хранения истории потерь\n",
"loss_history = []\n",
"\n",
"for epoch in range(epochs):\n",
" \n",
" # Прямой проход\n",
" s1 = np.dot(X, w1) + b1\n",
" z1 = activate_function_1(s1)\n",
" \n",
" s2 = np.dot(z1, w2) + b2\n",
" z2 = activate_function_2(s2)\n",
" \n",
" model_answer_interpretation = (z2 >= 0.5).astype('int')\n",
" \n",
" # Вычисляем ошибку\n",
" loss = loss_function(y_true, z2)\n",
" loss_history.append(loss)\n",
" \n",
" # Обратное распространение (backprop)\n",
" # Выходной слой\n",
" dL_dz2 = loss_derivative(z2, y_true)\n",
" dz2_ds2 = activate_derivative_2(s2)\n",
" dL_ds2 = dL_dz2 * dz2_ds2\n",
" \n",
" # Градиенты для w2 и b2\n",
" dL_dw2 = np.dot(z1.T, dL_ds2)\n",
" dL_db2 = np.sum(dL_ds2, axis=0, keepdims=True)\n",
" \n",
" # Переход к скрытому слою\n",
" dL_dz1 = np.dot(dL_ds2, w2.T)\n",
" dz1_ds1 = activate_derivative_1(s1)\n",
" dL_ds1 = dL_dz1 * dz1_ds1\n",
" \n",
" # Градиенты для w1 и b1\n",
" dL_dw1 = np.dot(X.T, dL_ds1)\n",
" dL_db1 = np.sum(dL_ds1, axis=0, keepdims=True)\n",
" \n",
" # Обновление скоростей\n",
" velocity_w1 = momentum * velocity_w1 + learning_rate * dL_dw1\n",
" velocity_b1 = momentum * velocity_b1 + learning_rate * dL_db1\n",
" velocity_w2 = momentum * velocity_w2 + learning_rate * dL_dw2\n",
" velocity_b2 = momentum * velocity_b2 + learning_rate * dL_db2\n",
" \n",
" # Обновление параметров\n",
" w1 -= velocity_w1\n",
" b1 -= velocity_b1\n",
" w2 -= velocity_w2\n",
" b2 -= velocity_b2\n",
" \n",
" # Вывод каждые 5 эпох\n",
" if (epoch + 1) % 5 == 0:\n",
"\n",
" clear_output(True)\n",
" plt.plot(range(1, epoch+2), loss_history, label='Loss')\n",
" plt.title(f'Epoch: {epoch + 1}, Loss: {loss:.6f}') \n",
" plt.grid(True, alpha=0.3)\n",
" plt.legend(loc='best')\n",
" plt.show()\n",
"\n",
" print('Test:')\n",
" for i in range(len(X)):\n",
" print(f'Input: {X[i]}, Output: {np.round(z2[i], 2)}, Pred: {model_answer_interpretation[i]}, True: {y_true[i]}')\n",
"\n",
" print(f'Accuracy: {accuracy(model_answer_interpretation, y_true):.2f}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Убедитесь, что сеть обучена хорошо — т.е. у неё высокое качество классификации, а в процессе обучения ошибка стаюильно снижалась:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"assert accuracy(model_answer_interpretation, y_xor_data) == 1\n",
"assert np.mean(loss_history[:5]) > np.mean(loss_history[-5:])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Поэкспериментируйте в процессе подбора параметров. Сделайте несколько вариантов с разными параметрами и выберите лучшие по качеству обучения модели, по её ошибке и по количеству эпох (т.е. как быстро она обучилась с такими параметрами).\n",
"\n",
"Отдельно проведите исследование, как на обучение сети влияют значения скорости обучение (`learning_rate`) и коэффициента импульса (`momentum`).\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Литература:\n",
"1. Бородкин А.А., Елисеев В.Л. Основы и применение искусственных нейронных сетей. Сборник лабораторных работ: методическое пособие. – М.: Издательский дом МЭИ, 2017.\n",
"2. MachineLearning.ru — профессиональный информационно-аналитический ресурс, посвященный машинному обучению, распознаванию образов и интеллектуальному анализу данных: http://www.machinelearning.ru\n",
"3. Modern State of Artificial Intelligence — Online Masters program at MIPT: https://girafe.ai/"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" "
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}