{
"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",
"\n",
"\n",
"В отличие от них, операция «исключающего ИЛИ» не является линейно разделимой. Для её реализации требуется как минимум два линейных классификатора (или нейронная сеть с одним скрытым слоем, содержащим два нейрона). Такая архитектура позволяет:\n",
" - сначала выделить промежуточные признаки (например, области «выше» и «ниже» двух разделяющих прямых);\n",
" - затем скомбинировать их для получения итогового результата «исключающего ИЛИ».\n",
"\n",
"\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
}