From 38d0109f5ef0a1d905e20b0dec4bd91d288ee18b Mon Sep 17 00:00:00 2001 From: NovikovVN Date: Thu, 19 Feb 2026 13:48:35 +0000 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?lab1=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lab1/1.1_numpy_solution.ipynb | 2247 +++++++++++++++++++++++++++++++ lab1/1.2_pytorch_solution.ipynb | 881 ++++++++++++ 2 files changed, 3128 insertions(+) create mode 100644 lab1/1.1_numpy_solution.ipynb create mode 100644 lab1/1.2_pytorch_solution.ipynb diff --git a/lab1/1.1_numpy_solution.ipynb b/lab1/1.1_numpy_solution.ipynb new file mode 100644 index 0000000..5f99921 --- /dev/null +++ b/lab1/1.1_numpy_solution.ipynb @@ -0,0 +1,2247 @@ +{ + "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. Работа искусственного нейрона" + ] + }, + { + "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. Обратное распространение ошибки" + ] + }, + { + "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", + "Первый шаг:" + ] + }, + { + "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. Решение задачи логического «И»" + ] + }, + { + "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. Решение задачи логического «ИЛИ»" + ] + }, + { + "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. Решение задачи логического «исключающего ИЛИ»" + ] + }, + { + "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", + "Первый шаг:" + ] + }, + { + "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 +} diff --git a/lab1/1.2_pytorch_solution.ipynb b/lab1/1.2_pytorch_solution.ipynb new file mode 100644 index 0000000..f2d1d15 --- /dev/null +++ b/lab1/1.2_pytorch_solution.ipynb @@ -0,0 +1,881 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### ЛАБОРАТОРНАЯ РАБОТА №1.2\n", + "## ИЗУЧЕНИЕ ОСНОВНЫХ ПОНЯТИЙ ТЕОРИИ ИСКУССТВЕННЫХ НЕЙРОННЫХ СЕТЕЙ" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> Цель работы: изучение основных понятий теории искусственных нейронных сетей на примере простых задач распознавания логических функций («И», «ИЛИ», «исключающее ИЛИ»)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Импорт библиотек:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bvIkPEqTXcpG" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import torch\n", + "import matplotlib.pyplot as plt\n", + "from IPython.display import clear_output\n", + "\n", + "from torch import nn\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "В заключительной части данной лабораторной работы мы применяем PyTorch — современный фреймворк для научных вычислений и глубокого обучения в Python.\n", + "\n", + "PyTorch во многом напоминает NumPy по принципам работы и синтаксису:\n", + "- оперирует многомерными массивами (в PyTorch они называются *тензорами*, или *Tensors*), которые являются прямым аналогом `ndarray` из NumPy;\n", + "- поддерживает похожий синтаксис для базовых операций: индексация, срезы, арифметические действия;\n", + "- реализует векторизованные вычисления и механизм broadcasting (широковещательное сложение);\n", + "- позволяет выполнять стандартные математические операции с интуитивно понятными методами (например, `tensor.mean()` в PyTorch аналогичен `array.mean()` в NumPy).\n", + "\n", + "\n", + "Некоторые основные компоненты PyTorch:\n", + "\n", + "1. Тензоры (Tensors) \n", + " - многомерные массивы с поддержкой вычислений на GPU;\n", + " - аналог `numpy.ndarray`, но с расширенными возможностями для машинного обучения.\n", + "\n", + "\n", + "2. Автоматическое дифференцирование (Autograd) \n", + " - механизм для автоматического вычисления градиентов;\n", + " - критически важен для обучения нейронных сетей (реализует алгоритм обратного распространения ошибки). \n", + " \n", + "\n", + "3. Модули (nn.Module) \n", + " - базовые строительные блоки для создания нейронных сетей;\n", + " - включают слои, функции активации, функции потерь и др.\n", + "\n", + "\n", + "4. Оптимизаторы (optim) \n", + " - реализации популярных алгоритмов оптимизации: SGD, Adam, RMSprop и др.;\n", + " - упрощают обновление параметров модели.\n", + "\n", + "\n", + "Ключевые возможности PyTorch:\n", + "\n", + "- Создание и обучение нейронных сетей любой сложности — от простых перцептронов до трансформеров.\n", + "- Вычисления на CPU и GPU с минимальным изменением кода (достаточно переместить тензоры на устройство `cuda`).\n", + "- Динамические вычислительные графы — в отличие от статических графов в некоторых других фреймворках, PyTorch строит граф операций «на лету», что упрощает отладку и эксперименты.\n", + "- Гибкость в проектировании архитектур — легко создавать кастомные слои и модели, переопределять методы обратного прохода.\n", + "\n", + "\n", + "Преимущества для исследований и разработки:\n", + "\n", + "- Интуитивный интерфейс, близкий к NumPy — низкий порог входа для тех, кто знаком с NumPy.\n", + "- Гибкая система наследования — возможность создавать собственные модули и слои.\n", + "- Обширное сообщество — множество туториалов, примеров, библиотек и форумов.\n", + "- Совместимость с другими библиотеками — лёгкая интеграция с NumPy, SciPy, Matplotlib и др.\n", + "- Производительность — ускорение вычислений на GPU (в 10–100 раз по сравнению с CPU для крупных задач).\n", + "\n", + "\n", + "Официальная документация: https://pytorch.org/docs/stable/index.html" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "В качестве примера вышесказанного переписанная на PyTorch метрика accuracy: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "l4lzUxFY0ej5" + }, + "outputs": [], + "source": [ + "def accuracy(y_pred, y_true):\n", + " return torch.sum(y_pred == y_true) / len(y_true)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Содержание: \n", + "[1. Решение задачи логического «И»](#p_1) \n", + "[2. Решение задачи логического «ИЛИ»](#p_2) \n", + "[3. Решение задачи логического «исключающего ИЛИ»](#p_3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Решение задачи логического «И»" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Представим входные данные для решения задачи логического «И» в виде тензора `X_data`.\n", + "\n", + "Операции в нейронных сетях (умножение весов, активация, вычисление градиентов) выполняются только с вещественными числами, поэтому через `.float()` приводим тензор к вещественному типу данных — иначе далее возможна ошибка типа данных." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Ej0DmW9uXlG0", + "outputId": "b3821094-83d4-40f1-faa6-7773535b9ea6" + }, + "outputs": [], + "source": [ + "X_data = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1]]).float()\n", + "print(X_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Аналогичная операция с выходным вектором:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "khRrzE2fXlR4", + "outputId": "a1aa980e-fa15-4c7c-ba68-18f0f5588f7f" + }, + "outputs": [], + "source": [ + "y_and_data = torch.tensor([0, 0, 0, 1]).reshape(-1, 1).float()\n", + "print(y_and_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Создание нейронной сети в PyTorch: последовательная модель**\n", + "\n", + "Один из самых простых и наглядных способов создать нейронную сеть в PyTorch — использовать `nn.Sequential`. Это контейнер, который организует слои в последовательный пайплайн: данные проходят через каждый слой по порядку, от входа к выходу — выход предыдущего слоя автоматически становится входом следующего.\n", + "\n", + "Создадим данным способом нейронную сеть `model_seq` с одним слоем нейронов.\n", + "\n", + "Пояснения к выбранным слоям в её архитектуре:\n", + " 1. `nn.Linear(in_features=2, out_features=1)` — полностью соединённый (плотный, полносвязный) слой. \n", + " \n", + " Параметры:\n", + " - `in_features=2` — размерность входного вектора (2 признака, соответствующие входам логической операции);\n", + " - `out_features=1` — размерность выхода (1 нейрон для итогового результата).\n", + " \n", + " Математическая операция: \n", + " $$\n", + " y = W \\cdot X + b,\n", + " $$ \n", + "\n", + " 2. `nn.Sigmoid()` — функция активации (сигмоида).\n", + " \n", + "После создания экземпляр `model_seq` можно:\n", + "- вызывать как функцию: `model_seq(X_data)` — прямой проход через сеть;\n", + "- получать параметры: `model_seq.parameters()` — для оптимизации;\n", + "- выводить структуру: печать `model_seq` покажет последовательность слоёв." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "gqr4pECOoDpG", + "outputId": "145244ca-2ede-498b-8a01-9841f6df041e" + }, + "outputs": [], + "source": [ + "model_seq = nn.Sequential(\n", + " nn.Linear(in_features=2, out_features=1),\n", + " nn.Sigmoid()\n", + ")\n", + "\n", + "model_seq" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Переберём все обучаемые параметры сети `model_seq` — результатом будет список тензоров, содержащих исходные веса и смещения сети:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "32TRT1kv9_Ln", + "outputId": "49e48ed3-3909-46d6-ac66-c0b59d1b9a5b" + }, + "outputs": [], + "source": [ + "[x for x in model_seq.parameters()]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Обратите внимание в выводе на флаг `requires_grad=True`.\n", + "\n", + "Он указывает, что для этого тензора в процессе обратного распространения ошибки нужно будет вычислять градиенты (производные) всех математических операций, через которые далее в коде этот тензор проходит. Без этого флага параметры не будут обновляться при обучении. Флаг при задании тензора также можно переключать на `False`.\n", + "\n", + "Как это работает:\n", + "\n", + " 1. Во время прямого прохода (`forward`) PyTorch строит вычислительный граф, запоминая операции с тензорами, у которых `requires_grad=True`.\n", + " 2. При вызове `loss.backward()` автоматически вычисляются градиенты по всем параметрам с `requires_grad=True`, при этом значение каждое градиенты сохраняются в атрибуте `.grad` тензора.\n", + " 3. Оптимизатор (объект, реализующий алгоритм обновления параметров модели на основе вычисленных градиентов — например, SGD) использует эти градиенты для обновления параметров." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Рассмотрим всё на примере.\n", + "\n", + "Выберем в качестве оптимизатора параметров `model_seq` SGD (с достаточно большим шагом обучения `lr` для наглядности) и среднеквадратичную функцию потерь:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DMJYrTlE-VqS" + }, + "outputs": [], + "source": [ + "optimizer = torch.optim.SGD(model_seq.parameters(), lr=1.5)\n", + "criterion = nn.MSELoss()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Пропустим входные данные `X_data` через `model_seq`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "5xiqGW-opJNK", + "outputId": "44183227-fe44-45f7-fb95-3dff575d702e" + }, + "outputs": [], + "source": [ + "z = model_seq(X_data)\n", + "z" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Атрибут `grad_fn=` в выводе обозначает:\n", + "- Тензор `z` — результат работы сигмоиды в последнем слое сети;\n", + "- PyTorch «запомнил» эту операцию в своём вычислительном графе;\n", + "- При обратном распространении ошибки градиенты будут корректно рассчитаны по формуле производной сигмоиды и переданы дальше в сеть." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Рассчитаем значение функции потерь:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "kn_XCMP8-fVm", + "outputId": "f5813ee6-a1f3-4b34-acee-77978ecf7b8f" + }, + "outputs": [], + "source": [ + "loss = criterion(z, y_and_data)\n", + "loss" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "В атрибуте `grad_fn=` аналогично видно, что в вычислительном графе к тензорам применена функция среднеквадратичной ошибки.\n", + "\n", + "Вычислим градиенты функции потерь по всем параметрам модели согласно вычислительному графу:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kWc5ip2O-wKp" + }, + "outputs": [], + "source": [ + "loss.backward()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Обновим с помошью оптимизатора все параметры сети: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dxh8RIJV-1Hy" + }, + "outputs": [], + "source": [ + "optimizer.step()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Посмотрим значения обновлённых параметров:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "AQt5IglN_BJx", + "outputId": "b916e29e-874d-4396-a447-3556d5b5757e" + }, + "outputs": [], + "source": [ + "[x for x in model_seq.parameters()]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Метод `optimizer.zero_grad()` вызывается обязательно перед каждым последующим обратным проходом (`loss.backward()`) в цикле обучения для **обнуления градиентов всех параметров** модели, управляемых этим оптимизатором.\n", + "\n", + "Почему это необходимо? \n", + "\n", + "В PyTorch градиенты по умолчанию накапливаются (суммируются) при каждом вызове `loss.backward()`. Если не обнулять градиенты:\n", + "- значения градиентов будут расти с каждой итерацией;\n", + "- веса модели начнут обновляться некорректно (с учётом «старых» градиентов);\n", + "- возможен эффект «взрывных градиентов» (gradients explode), когда значения становятся настолько большими, что превращаются в `NaN` (не число)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KRD01eVD_yjE" + }, + "outputs": [], + "source": [ + "optimizer.zero_grad()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Сделайте несколько проходов (итераций) от [первого шага обратного распространения ошибки](#loop_and) до данной ячейки. Убедитесь, что ошибка сети `loss` понижается." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Рассмотрим ещё один способ создания сети в PyTorch.\n", + "\n", + "**Создание нейронной сети в PyTorch: через ООП**\n", + "\n", + "В PyTorch принято определять архитектуры нейронных сетей как классы, наследующие от `nn.Module`. Это позволяет:\n", + "- структурировать код в виде повторно используемых компонентов;\n", + "- чётко разделять инициализацию параметров (`__init__`) и прямой проход (`forward`);\n", + "- легко расширять и модифицировать модели;\n", + "- использовать встроенные механизмы PyTorch (оптимизаторы, сохранение/загрузка моделей).\n", + "\n", + "Пример сети с одним полносвязным слоем и сигмоидой в качестве функции активации:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PF5yli7WXlVd" + }, + "outputs": [], + "source": [ + "class OneLayerNetwork(nn.Module):\n", + " def __init__(self, n_inputs=2, n_outputs=1):\n", + " super().__init__()\n", + " self.model = nn.Sequential(\n", + " nn.Linear(in_features=n_inputs, out_features=n_outputs),\n", + " nn.Sigmoid()\n", + " )\n", + "\n", + " def forward(self, X):\n", + " return self.model(X)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Сейчас все слои для удобства по-прежнему собраны в `nn.Sequential`. Чтобы повысить гибкость модели, в методе `__init__` можно по отдельности задать все необходимые слои, а в `forward` — определять порядок их применения. Такой способ даёт больше контроля над архитектурой (пример разберём в одной из следующих лабораторных работ).\n", + "\n", + "Сигмоиду здесь можно также заменить на `nn.ReLU` или другую функцию активации — но для отделения от экземплятор с исходной архитектурой создайте другой класс (например, `OneLayerReLUNetwork()`)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Создадим экземпляр однослойной сети этого класса для решения задачи логического «И»:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "kIuQLzUIwVwC", + "outputId": "91aa25b7-aae4-4655-e83b-4abac88531c8" + }, + "outputs": [], + "source": [ + "model_one_layer = OneLayerNetwork()\n", + "model_one_layer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "По аналогии с прошлым ноутбуком зададим \n", + " - данные для обучения (входные и выходные);\n", + " - экземпляр нейронной сети (через последовательность `nn.Sequential()` или класс `OneLayerNetwork()`);\n", + " - количество эпох или итераций в процессе обучения (желательно больше 5);\n", + " - скорость обучение и коэффициент импульса;\n", + " - оптимизатор (попробуйте разные алгоритмы — `torch.optim.SGD()`, `torch.optim.Adam()` и другие) (значение `learning_rate` передаётся в `lr`) (учтите, что в `torch.optim.Adam()` нет импульса);\n", + " - функцию потерь (можно оставить `nn.MSELoss()`, однако поскольку решается задача бинарной классификации, можно выбрать бинарную кросс-энтропию `nn.BCELoss()`).\n", + " \n", + " \n", + "*Примечание*. Вы можете использовать для обучения сети либо код ниже, либо модифицированный на его основе — например, реализовать функцию обучения нейронной сети и проверки её работы." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "p09jzhOY3MyQ" + }, + "outputs": [], + "source": [ + "y_true = y_and_data\n", + "\n", + "# Перебор seed для инициализации параметров\n", + "torch.manual_seed(seed=42)\n", + "\n", + "model = # Ваш код здесь\n", + "\n", + "epochs = # Ваш код здесь\n", + "\n", + "learning_rate = # Ваш код здесь\n", + "momentum = # Ваш код здесь\n", + "\n", + "optimizer = # Ваш код здесь\n", + "criterion = # Ваш код здесь" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Обучение нейронной сети для решения задачи логического «И»:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 556 + }, + "id": "qKimvoStsZXv", + "outputId": "725ec1a2-2d68-4f24-91c6-3469a39efbc4" + }, + "outputs": [], + "source": [ + "loss_history = []\n", + "\n", + "for epoch in range(epochs):\n", + " \n", + " # Метод .train() переводит модель в режим обучения\n", + " model.train()\n", + "\n", + " optimizer.zero_grad()\n", + "\n", + " z = model(X_data)\n", + " model_answer_interpretation = (z >= 0.5).float()\n", + "\n", + " loss = criterion(z, y_true)\n", + " \n", + " # Метод .item() извлекает скалярное значение из тензора loss\n", + " loss_history.append(loss.item())\n", + "\n", + " loss.backward()\n", + "\n", + " optimizer.step()\n", + " \n", + " # Метод .eval() переводит модель в режим валидации/тестирования (об этом в следующей лабораторной)\n", + " model.eval()\n", + "\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_data)):\n", + " # Метод .detach() создаёт копию тензора, отсоединённую от вычислительного графа,\n", + " # чтобы избежать ненужного вычисления градиентов для исходного тензора\n", + " print(f'Input: {X_data.numpy()[i]}, Output: {torch.round(z[i].detach() * 100).numpy() / 100}, Prediction: {model_answer_interpretation[i].numpy()}, True: {y_true[i].numpy()}')\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": [ + "Поэкспериметируйте с параметрами обучения — сделайте несколько вариантов с разными функциями потерь, оптимизаторами, значениями скорости обучения и коэффициента импульса. Выберите лучшие по качеству обучения и количеству эпох." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Решение задачи логического «ИЛИ»" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "По аналогии с задачей логического «И» решите задачу логического «ИЛИ».\n", + "\n", + "Выходные данные:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "UGFSsTGh4Xyp", + "outputId": "40e554c0-51a0-4adb-ca8b-8e46c83c2e7d" + }, + "outputs": [], + "source": [ + "y_or_data = torch.tensor([0, 1, 1, 1]).reshape(-1, 1).float()\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": [ + "Поэкспериметируйте с параметрами обучения — сделайте несколько вариантов с разными функциями потерь, оптимизаторами, значениями скорости обучения и коэффициента импульса. Выберите лучшие по качеству обучения и количеству эпох." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Решение задачи логического «исключающего ИЛИ»" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Выходные данные:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "gNigOR9N5X3z", + "outputId": "297e81ad-0eb7-403a-8dbf-0df7706c934e" + }, + "outputs": [], + "source": [ + "y_xor_data = torch.tensor([0, 1, 1, 0]).reshape(-1, 1).float()\n", + "print(y_xor_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6hL-mvyF5T58" + }, + "source": [ + "Напомним, что однослойной сетью задачу «исключающего ИЛИ» решить нельзя. В нейронную сеть требуется добавить ещё один слой с как минимум двумя нейронами.\n", + "\n", + "Модифицируем для этого класс `OneLayerNetwork()` в класс `TwoLayersNetwork()` — в последовательсти в `self.model` добавим ещё один полносвязный слой с сигмоидой для активации. Добавлен также параметр `n_hiddens` — количество нейронов в скрытом слое. Обратите внимание на задание размерности в последовательности слоёв." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TGcgUAFQtXn7" + }, + "outputs": [], + "source": [ + "class TwoLayersNetwork(nn.Module):\n", + " def __init__(self, n_inputs=2, n_hiddens=2, n_outputs=1):\n", + " super().__init__()\n", + " self.model = nn.Sequential(\n", + " nn.Linear(in_features=n_inputs, out_features=n_hiddens),\n", + " nn.Sigmoid(),\n", + " nn.Linear(in_features=n_hiddens, out_features=n_outputs),\n", + " nn.Sigmoid()\n", + " )\n", + "\n", + " def forward(self, X):\n", + " return self.model(X)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Создадим экземпляр двухслойной сети этого класса для решения задачи «исключающего ИЛИ»:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "afK0vsG1t2oL", + "outputId": "bab9683b-fefe-4734-f948-340a353b99c8" + }, + "outputs": [], + "source": [ + "model_two_layers = TwoLayersNetwork()\n", + "model_two_layers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "По аналогии с задачами логического «И» и логического «ИЛИ» решите «задачу исключающего ИЛИ»:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Ваш код здесь\n", + "\n", + "# Вариант задания сети с разным количество нейронов в скрытом слое\n", + "n_hiddens = 2\n", + "model = TwoLayersNetwork(n_hiddens=n_hiddens)\n", + "\n", + "# Ваш код здесь" + ] + }, + { + "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": [ + "Поэкспериметируйте с параметрами обучения — сделайте несколько вариантов с разными функциями потерь, оптимизаторами, значениями скорости обучения и коэффициента импульса. Выберите лучшие по качеству обучения и количеству эпох." + ] + }, + { + "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": { + "colab": { + "provenance": [] + }, + "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": 1 +}