{ "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 }