Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

3113 строки
148 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
" <font size=\"6\">Нейронные сети</font>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Ограничения Линейного классификатора"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вспомним материал лекции №2:\n",
"\n",
"Мы обучали линейный классификатор на датасете CIFAR-10\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/input_img_scalar_product_add_bias_get_scores.jpg\" width=\"600\">\n",
"\n",
"\n",
"\n",
"\n",
"- Основная операция линейного классификатора: скалярное произведение\n",
"- Функции потерь: SVM Loss, Cross-Entropy Loss\n",
"- Метод обучения: градиентный спуск\n",
"- Оценка точности линейного классификатора на CIFAR-10: ~0.38\n",
"\n",
"Попробуем визуализировать шаблоны (матрицы весовых коэффициентов), получающиеся в результате обучения линейного классификатора. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from IPython.display import clear_output\n",
"!wget https://edunet.kea.su/repo/EduNet-web_dependencies/L05/lc_cifar10_weights.txt\n",
"clear_output()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# Display templates\n",
"plt.rcParams[\"figure.figsize\"] = (25, 10)\n",
"\n",
"W = torch.from_numpy(np.loadtxt(\"lc_cifar10_weights.txt\")) # load weigths, shape 3073x10\n",
"print(f'Shape with bias: {W.shape}')\n",
"\n",
"# Remove bias\n",
"W = W[:-1, :]\n",
"print(f'Shape without bias: {W.shape}')\n",
"\n",
"# Denormalize\n",
"w_min = torch.min(W)\n",
"w_max = torch.max(W)\n",
"templates = 255 * (W - w_min) / (w_max - w_min)\n",
"\n",
"# Display templates\n",
"labels_names = ['plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']\n",
"for i in range(10):\n",
" plt.subplot(1, 10, i+1)\n",
" img = templates[:,i].view(3, 32, 32).permute(1, 2, 0).type(torch.uint8)\n",
" plt.imshow(img)\n",
" plt.axis('off')\n",
" plt.title(labels_names[i])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Ограничение линейного классификатора состоит в том, что для каждого класса существует только один шаблон. Шаблон каждого класса будет пытаться вобрать в себя информацию обо всех объектах класса сразу (например, на получившихся шаблонах у лошади две головы, машина красная и т. д.). Сильная внутриклассовая вариативность будет мешать линейному классификатору запоминать разные варианты объектов одного класса, и это ограничивает точность модели."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## ХОR — проблема"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"У линейного классификатора есть существенные ограничения применения. Рассмотрим задачу XOR. На вход подаётся упорядоченный набор из двух чисел согласно таблице истинности логической функции \"исключающее ИЛИ\" (XOR). Задача линейного классификатора — сопоставить этим числам их класс согласно таблице. Графически два входных числа можно изобразить как координаты точек на плоскости, а цветом обозначить их истинный класс. Тогда задача классификатора — построить линию, отделяющую красные точки (класс 0) от зелёных точек (класс 1). Однако видно, что одной линией это сделать геометрически невозможно — точки, размеченные по таблице истинности XOR являются **линейно неразделимыми**.\n",
"\n",
"То есть, линейный классификатор уже не может справиться с этой, казалось бы, простой задачей."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/xor_problem.png\" width=\"600\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Проблемы классификации более сложных объектов"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Человек узнает на изображении кошку или любой другой объект, руководствуясь целостным представлением о данном объекте на изображении. Такое целостное интуитивное представление об объектах для компьютера напрямую недоступно. С точки зрения компьютера, изображение представляет собой не более чем таблицу из чисел, кодирующих цвета всех его пикселей. Небольшое цветное изображение (с тремя цветовыми каналами: красным, зеленым и синим) в разрешении $32 \\times 32$ для компьютера представлено просто упорядоченным набором из $32 \\times 32 \\times 3 = 3072 $ целых чисел."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Легко себе представить ситуацию, в которой изображения одного и того же объекта будут значительно отличаться на масштабе отдельных пикселей. Так, например, один и тот же кот может быть представлен на фотографии в различных позах, фотографии могут отличаться условиями освещения, яркостью или контрастностью. Кроме того, на одной из фотографий может быть изображен только фрагмент объекта — скажем, только хвост. Все эти факторы не являются преградой для распознавания человеком, и мы хотим потребовать того же и для реализованных на компьютере алгоритмов классификации.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вот лишь малая часть параметров, которые будут влиять на точность распознавания классификатора:\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/problem_classification_difficult_object.png\" width=\"700\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Все описанные выше сложности обобщенно можно назвать **внутриклассовой вариативностью**: мы можем приписывать к одному классу объекты, которые допускают широкий спектр определения. Так, например, мы обобщаем классом \"кошка\" кошек различных пород, размеров и возрастов. \"Хороший\" алгоритм классификации должен быть устойчив к внутриклассовой вариативности и верно распознавать все возможные варианты объектов."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Один из подходов к решению этой проблемы — модифицировать модель таким образом, чтобы на у нее внутри было не по одному шаблону на каждый класс, а по несколько (скажем, вместо 100 шаблонов вместо 10 при десяти классах). Тогда бы Ямодель имела возможность запоминать разные объекты одного класса и далее использовать эти промежуточные шаблоны для разбиения объектов на классы."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/modified_model.png\" width=\"700\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Реализуем эту модель на основе линейного классификатора из лекции №2:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Применяем к выходам классификатора еще один классификатор. Будет ли работать данная модель?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"x = torch.rand(3072) # random image\n",
"W1 = torch.randn(3072, 100) * 0.0001 # without bias \n",
"W2 = torch.randn(100, 10) * 0.0001 # without bias \n",
"scores1 = x.matmul(W1) # matrix multiplication, equivalent x@W1\n",
"scores2 = scores1.matmul(W2) # matrix multiplication, of the next classifier\n",
"\n",
"print(f'First classifier shape: {scores1.shape}')\n",
"print(f'Second classifier shape: {scores2.shape}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Нетрудно заметить, что последовательное применение двух классификаторов к входным данным эквивалентно применению одного классификатора с матрицей весов, равной произведению двух матриц весов классификаторов примененных последовательно.\n",
"\n",
"$$ scores_1 = W_1 \\cdot x $$\n",
"\n",
"$$ scores_2 = W_2 \\cdot scores_1 = W_2 \\cdot W_1 \\cdot x $$ \n",
"\n",
"$$ W = W_2 \\cdot W_1 $$\n",
"\n",
"$$ scores_2 = W \\cdot x $$ \n",
"\n",
"Для того, чтобы последовательно примененные классификаторы не вырождались в один, необходимо применить нелинейность к их выходам, например, сделаем так, чтобы каждый шаблон, предсказывающий класс объекта воспринимал только положительные сигналы с выхода первого классификатора:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"scores1 = x.matmul(W1) \n",
"print(f\"\\nFirst 8 elements of Scores1: {scores1[:8]}\") # take the first 8 values for visualization\n",
"activations = torch.maximum(torch.tensor(0), scores1) # only values greater than zero\n",
"print(f\"\\nActivations {activations[:8]}\" ) # take the first 8 values for visualization\n",
"scores2 = activations.matmul(W2)\n",
"print(f\"\\nScores2 {scores2}\") "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Теперь вычисления выглядят так:\n",
"\n",
"$$ scores_1 = W_1 \\cdot x $$\n",
"\n",
"$$ activations = max(0, scores_1) $$\n",
"\n",
"$$ scores_2 = W_2 \\cdot activations = W_2 \\cdot max(0, scores_1)$$ "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Нелинейность:\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/non_linearity.jpg\" width=\"450\">\n",
"\n",
"Такая конструкция называется **функцией активации**. И мы уже пользовались подобной, когда разбирали Cross-Entropy loss (Softmax).\n",
"\n",
"Приведем код в порядок:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class NeuralNet():\n",
" def __init__(self):\n",
" self.W1 = torch.randn(3072, 100)*0.0001 \n",
" self.W2 = torch.randn(100, 10)*0.0001 \n",
"\n",
" def predict(self, x):\n",
" scores1 = x.matmul(self.W1) # Linear\n",
" activations1 = torch.maximum(torch.tensor(0), scores1) # activation ReLU \n",
" scores2 = activations1.matmul(self.W2) # Linear\n",
" return scores2\n",
"\n",
"x = torch.rand(3072) # image\n",
"model = NeuralNet()\n",
"scores = model.predict(x) \n",
"print(f'scores: \\n {scores}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Ядром вычислений по-прежнему является скалярное произведение входов с весовыми коэффициентами. \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/dot_product.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/input_weght_out.png\" width=\"700\">\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"И оно соответствует одному слою искусственной нейронной сети (за исключением функции активации)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/scalar_product_add_bias.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Многослойные сети"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"По мере развития мощности компьютеров, теоретической базы, появления больших датасетов и метода обратного распространения ошибки, появилась возможность строить более сложные сети — многослойные нейронные сети или же в современном понимании просто **нейронные сети**."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Пример **полносвязной (fully-connected network)** нейронной сети с двумя скрытыми слоями:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/nn_fully_connected.png\" width=\"400\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Обучение нейронной сети"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Нейронная сеть в процессе **обучения** последовательно обрабатывает все объекты из обучающей выборки.\n",
"Предъявление нейронной сети всех объектов обучающей выборки по одному разу называется **эпохой обучения**. \n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Обучающую выборку разделяют на две части: непосредственно использующуюся для обучения (train data) и валидационную (validation data). На валидационных данных каждую эпоху происходит оценка качества обучения. Стратегия разделения на train и validation подвыборки может быть произвольной, но при разделении следует заботиться о том, чтобы эти подвыборки были \"похожи\". \n",
"\n",
"В ходе обучения ошибка работы модели измеряется на обучающих и валидационных данных для контроля **переобучения** (overfitting). В случае возникновения переобучения нейронная сеть начинает терять обобщающую способность, что можно заметить по возникновению роста ошибки на валидационной подвыборке в ходе процесса обучения — в случае переобучения нейронная сеть просто \"зазубривает\" примеры из обучающей подвыборки, а не аппроксимирует искомую функциональную зависимость между признаками и целевой переменной."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Прямое и обратное распространение"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим процесс **обучения с учителем** (supervised learning) нейронной сети с прямым распространением сигнала (feedforward neural network). В ходе такого процесса мы хотим аппроксимировать при помощи нейронной сети функциональную зависимость между некоторым набором входных сигналов (признаков) и соответствующим им набором выходных сигналов (целевой переменной), используя множество эталонных пар вход-выход. \n",
"\n",
"На этапе **прямого распространения** (forward pass), нейронной сети на вход подаются объекты из тренировочной выборки, вычисляется выход сети, и при помощи **функции потерь** (loss function) производится количественное сравнение полученных на выходе нейронной сети сигналов с эталонными значениями выхода. Далее, на этапе **обратного распространения** (backward pass), значение функции потерь будет использовано для постройки параметров сети.\n",
"\n",
"Подстройка параметров нейронной сети может происходить после вычисления функции потерь на одном примере (online) или же после накопления информации об отклике сети на пакете из нескольких примеров из обучающей выборки (**mini-batch**).\n",
"\n",
"После завершения эпохи обучения и подстройки параметров модели происходит вычисление функции потерь на валидационных данных. Для валидационных данных производится только прямое распространение и вычисление функции потерь. **Подстройка параметров на валидационных данных не происходит.**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Веса сети"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Нейрон** является базовым элементом строения нейронной сети. У нейрона есть определенное количество \"входов\", которыми он \"подключён\" к выходным значениям других нейронов. Нейрон осуществляет суммирование приходящих в него входных значений, причём учитывает значения входов с определенными весовыми коэффициентами (или просто **весами**), которые в определенном смысле характеризуют их значимость. Веса сети являются вещественными числами и могут быть как положительными, так и отрицательными. Именно веса нейрона являются изменяемыми параметрами и они подвергаются подстройке во время обучения нейронной сети."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Как вычислить результат работы нейронной сети"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/nn__xor_example.png\" width=\"600\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим прямое распространение сигнала на одном примере задачи XOR. При подаче на вход $1$ и $0$, мы будем ожидать $1$ на выходе. Веса сети определим случайным образом:\n",
"\n",
"$I_1=1\\quad I_2=0$\n",
"\n",
"$w_1=0.45\\quad w_2=0.78\\quad \n",
"w_3=-0.12\\quad w_4=0.13$\n",
"\n",
"\n",
"$w_5=1.5\\quad w_6=-0.3$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вычислим выходные значения так называемых \"скрытых\" (hidden) нейронов $H_1$ и $H_2$. На вход они получают входные (input) значения $I_1$ и $I_2$ , которые умножаются на соответствующие веса $w_1$, $w_2$, $w_3$ и $w_4$."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$ H_1 = w_1 I_1 + w_3 I_2 = 0.45 * 1 + (-0.12) * 0 = 0.45 $\n",
"\n",
"$ H_2 = w_2 I_1 + w_4 I_2 = 0.78 * 1 + 0.13 * 0 = 0.78 $"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Для того, чтобы добавить сети выразительной способности, выходные значения скрытых нейронов $H_1$ и $H_2$ пропускаются через **нелинейную функцию активации**. В данном примере в качестве функции активации используется сигмоида $\\sigma(x)$. \n",
"\n",
"Подробнее об этой и других функциях активации мы поговорим далее в этой лекции, а пока давайте рассмотрим график сигмоиды:\n",
"\n",
"$$\\sigma(x)=\\frac{1}{1+e^{-x}}$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/sigmoid_function.png\" width=\"500\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Одним из свойств сигмоидальной функции активации является то, что она переводит аргумент $x$, определенный на всей вещественной прямой от $-\\infty$ до $+\\infty$, в значение из интервала $(0, 1)$."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Итак, вернемся к расчетам:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$ H_1^{out} = \\sigma(H_1) = \\sigma(0.45) = 0.61 $\n",
"\n",
"$ H_2^{out} = \\sigma(H_2) = \\sigma(0.78) = 0.69 $"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Теперь мы можем вычислить значение выходного (output) нейрона $O_1$. Он также получает на вход сигналы от нейронов скрытого слоя $H_1^{out}$ и $H_2^{out}$, которые умножаются на соответствующие веса $w_5$ и $w_6$, и складываются"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$O_1 = w_5 H_1^{out} + w_6 H_2^{out} = 1.5 * 0.61 + (-0.3) * 0.69 = 0.71$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Значение взвешенной суммы может меняться от $-\\infty$ до $+\\infty$, а мы ожидаем на выходе модели значение от $0$ до $1$ (вспомним, что мы решаем задачу XOR). Поэтому взвешенная сумма, полученная в $O_1$, также пропускается через сигмоидальную функцию активации, и это значение является выходом всей нейронной сети."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$O_1^{out} = \\sigma(O_1) = σ(0.71) = 0.67 $ "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Таким образом, мы произвели прямое распространение сигнала от входа нейронной сети и получили значение на выходе. Ответ нейронной сети $O_1^{out} = 0.67$, а мы ожидали на выходе $1$. О том, как скорректировать веса нужным образом будет рассказано в разделе о методе обратного распространения ошибки."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Смещение (bias)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/why_add_bias_example.png\" width=\"500\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим простой пример. На вход нейрона подаётся входное значение $x$, умноженное на вес $w$. После применения функции активации, в зависимости от веса, при всевозможных значениях входа мы можем получить следующие графики при $w$ равном $0.5$, $1$ и $2$:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-web_dependencies/L05/non_bias_problem_plot.png\" width=\"600\">\n",
"\n",
"<center><p><em>Source: <a href=\"https://codeby.school/blog/informacionnaya-bezopasnost/pogrughenie-v-mashinnoe-obuchenie-chast-1\">Immersion in Machine learning</a></p> </em></center>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[A Biased Graph Neural Network Sampler](https://arxiv.org/pdf/2103.01089.pdf)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Но что если мы захотим, чтобы при $x=5$ нейрон выдавал $0$? Тогда без смещения эту задачу не решить.\n",
"\n",
"Просто изменить крутизну сигмоиды на самом деле не получится — мы хотим иметь возможность сдвинуть всю кривую вправо."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Смещение** (англ. bias) — это дополнительный коэффициент, прибавляющийся к взвешенной сумме входов. Наличие смещения позволяет сдвинуть функцию активации влево или вправо, что может иметь решающее значение для успешного обучения."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/add_bias_example.png\" width=\"500\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Тогда при разных смещениях $5$, $0$ и $-5$ мы можем получить сдвинутые функции активации, что способствует лучшему обучению нейронной сети:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-web_dependencies/L05/after_add_bias_plot.png\" width=\"600\">\n",
"\n",
"<center><p><em>Source: <a href=\"https://codeby.school/blog/informacionnaya-bezopasnost/pogrughenie-v-mashinnoe-obuchenie-chast-1\">Immersion in Machine learning</a></p> </em></center>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Метод обратного распространения ошибки"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Итак, мы поняли, какие действия необходимо выполнять внутри одной эпохи обучения двухслойной неронной сети:\n",
"- предобработать данные;\n",
"- умножить их на веса;\n",
"- применить функцию активации;\n",
"- снова умножить на веса;\n",
"- вычислить значение функции потерь;\n",
"- вычислить градиент функции потерь по весам;\n",
"- обновить веса;\n",
"- оценить точность.\n",
"\n",
"А как будем искать градиент?\n",
"Во второй лекции мы вручную считали производную от функции потерь по весам. Так как модель поменялась, придется делать это заново.\n",
"\n",
"Для того, чтобы упростить этот процесс, используется метод обратного распространения ошибки или backpropagation.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Метод обратного распространения ошибки (backpropagation)** является эффективным методом вычисления градиента от функции потерь многослойной нейронной сети. Благодаря данному методу становится практически возможным использование метода градиентного спуска для проведения процедуры обучения. Несмотря на то, что первые работы по обучению многослойных перцептронов методом обратного распространения ошибки были опубликованы ещё в 1974 году, значительное развитие данная технология получила сравнительно недавно, после появления современных вычислительных ресурсов."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Основная идея метода"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Метод обратного распространения ошибки явно использует структуру многослойной нейронной сети как сложной функции, применяя цепное правило дифференцирования для вычисления градиента от функции потерь по весам сети. Градиент от функции потерь нейронной сети вычисляется последовательно, при движении по вычислительному графу нейронной сети от её выходов в направлении входов. Именно такой порядок обхода **вычислительного графа** и обуславливает название метода."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Граф вычислений"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Любую нейронную сеть можно представить в виде графа \"последовательных действий\", где результат вычисляется последовательно, одно действие за другим."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Ранее мы вычисляли производную вручную, и для простой модели это было несложно.\n",
"\n",
"Однако по мере добавления слоев модель может оказаться намного сложнее. Например, так выглядит архитектура AlexNet:\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/alexnet.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Алгоритм обратного распространения ошибки позволяет находить градиенты для любого графа вычислений, если описываемая им функция дифференцируема.\n",
"\n",
"В его основе лежит правило взятия производной сложной функции (chain rule):"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Одна переменная:\n",
"$$ y(x) = f(u(x)) $$\n",
"\n",
"$$ \\frac{dy}{dx} = \\frac{df}{du} \\cdot \\frac{du}{dx}$$\n",
"\n",
"\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Несколько переменных:\n",
"\n",
"$$ y(x) = f(u_1(x),u_2(x),...u_n(x)) $$\n",
"\n",
"$$ \\frac{dy}{dx} = \\sum_{i=1}^{n} \\frac{\\partial f(u_1, u_2, ... u_n)}{\\partial u_i} \\frac{du_i}{dx}$$\n",
"\n",
"$$ \\underbrace{\\frac{d}{dt} f(\\vec{\\mathbf{v}}(t))}_{\\text{Derivative of composition function}} = \\overbrace{\\nabla f \\cdot \\vec{\\mathbf{v}}'(t)}^{\\text{Dot product of vectors}} $$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Пошаговый разбор метода обратного распространения"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Алгоритм, по которому вычисляются веса, можно представить в виде графа.\n",
"\n",
"А общее правило взятия градиентов можно представить следующим образом:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/rule_for_taking_gradients.png\" width=\"500\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим следующую функцию:\n",
"\n",
"$$f(w,x)=\\frac{1}{1+e^{-(w_0x_0+w_1x_1+w_2)}}$$\n",
"\n",
"Её можно представить в виде простого графа вычислений:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/graph_of_calculation_gradient.png\" width=\"700\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"На примере данной несложной функции рассмотрим алгоритм обратного распространения ошибки и найдём величину её градиента по параметрам $w$.\n",
"Нам потребуется вычислить частные производные $\\frac{\\partial f}{\\partial w_0}$, $\\frac{\\partial f}{\\partial w_1}$, $\\frac{\\partial f}{dw_2}$, $\\frac{\\partial f}{\\partial x_0}$ и $\\frac{\\partial f}{\\partial x_1}$. \n",
"\n",
"Пусть \"веса\" $w$ инициализированы значениями $w_0=2\\;w_1=-3,\\;w_2=-3,$а \"входные признаки\" $x$ принимают значения $x_0=-1.0,\\;x_1=-2.0$.\n",
"\n",
"Делая прямой проход через граф вычислений для данной функции, получаем её значение для заданных $w$ и $x$ равным $f=0.73$:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/forward_pass_example.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Далее, в соответствии с алгоритмом обратного распространения ошибки, рассчитаем частные производные, пройдясь последовательно по графу вычислений, постепенно накапливая искомое значение для градиента функции:\n",
"\n",
"Для начала посчитаем производную функции $\\frac{\\partial f}{\\partial f}$, которая будет равна единице. Движемся дальше по графу вычислений — следующая вершина содержит функцию $f(x)=\\frac{1}{x}$, производная которой равна $\\frac{df}{dx}=-\\frac{1}{x^2}$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/compute_gradient_1_step.png\" width=\"800\">\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В следующем узле находится функция $f(x)=1+x$. Производная от константы $+1$ равняется нулю, то есть производная от всего выражения в данном узле равняется просто $\\frac{df}{dx}=1$:\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/compute_gradient_2_step.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Третья вершина содержит экспоненту $f(x)=e^x$. Её производная также является экспонентой $\\frac{df}{dx}=e^x$:\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/compute_gradient_3_step.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Следующая вершина, четвертая, содержит умножение на константу $f(x)=ax$. Производная равна $\\frac{df}{dx}=a$ (в данном случае $a=-1$):\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/compute_gradient_4_step.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Двигаясь по графу вычислений, мы дошли до узла суммирования, который имеет два входа. Относительно каждого из входов локальный градиент в вершине суммирования будет равен $1$:\n",
"$$f(x,y)=x+y \\quad \\Rightarrow \\quad \\frac{\\partial f}{\\partial x}=1 \\quad \\quad \\frac{\\partial f}{\\partial y}=1$$\n",
"Так как умножение на единицу не изменит значения входного градиента, всем входам узла суммирования мы можем приписать точно такое же значение входного градиента ($0.2$), что мы имели и для самого узла суммирования. Будем действовать аналогично и со всеми остальными узлами суммирования, что встретятся нам в вычислительном графе.\n",
"\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/compute_gradient_5_step.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Двигаясь далее к началу графа вычислений, мы подходим к вершинам умножения. Для такой вершины локальный градиент по отношению к какому-либо из входов будет равен значению оставшегося входа. Остается умножить локальный градиент на входящий.\n",
"$$f(w,x)=wx \\quad \\Rightarrow \\quad \\frac{\\partial f}{\\partial w}=x \\quad \\quad \\frac{\\partial f}{\\partial x}=w$$\n",
"\n",
"Точно так же мы можем поступить и с оставшейся второй вершиной умножения, которая привязана к $w_1$ и $x_1$:\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/compute_gradient_6_step.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Так, двигаясь по графу вычислений в обратном направлении от выхода сети к входным признакам, мы последовательно для каждого узла умножаем локальный градиент на входящий градиент, используя цепное правило дифференцирования сложной функции. В описанном примере мы полностью разбили граф вычислений на отдельные элементарные узлы. Разбиение вычислительного графа на элементарные узлы вовсе не обязательно — мы можем сгруппировать несколько вершин вместе, если они образуют дифференцируемую функцию, и рассматривать их совместно. \n",
"\n",
"В нашем примере мы можем заметить, что вычислительный граф можно свести к двум операциям: получению выражения $w_0x_0+w_1x_1+w_2$ и последующему вычислению от него сигмоидальной функции. Важно отметить, что сигмоида обладает важным свойством: её производная может быть выражена через саму сигмоидалную функцию:\n",
"\n",
"$$\\frac{d\\sigma{(x)}}{dx}=(1-\\sigma{(x)}) \\cdot \\sigma{(x)}$$\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/compute_gradient_join_vertices_sigmoid_example.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В коде без использования библиотек подсчёт градиентов можно записать как:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/calculating_gradients_in_code.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Более сложные случаи"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Обратное распространение для векторов:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В случае, когда выход сети $z$ не один (многоклассовая классификация, тексты, картинки и т.д.), необходимо учитывать значения каждого элемента выходного вектора для расчёта градиентов."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Матрица Якоби — матрица, составленная из частных производных отображения."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/backpopagation_with_vectors.png\" width=\"700\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/backpropagation_with_vectors_example.png\" width=\"700\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Множественная вершина"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Если вход соединен с несколькими вершинами графа или у вершины больше одного выхода:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/multiple_vertices_of_graph.png\" width=\"700\">\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"то в месте ветвления можно создать дополнительную вершину, которая будет соответствовать операции копирования."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/add_copy_operation_for_vertex.png\" width=\"700\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Тогда при обратном распространении, градиент можно разделить в соответствии с осуществлённой операцией"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/upstream_gradient.png\" width=\"700\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Для базовых операций можно выделить следующие шаблоны \"разделения\" градиента:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/add_copy_mul_max_gates.png\" width=\"700\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Анимация работы метода обратного распространения ошибки"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"Таким образом, метод обратного распространения ошибки включает в себя следующие шаги:\n",
"* Forward pass (FP) - прямое распространение сигнала от входа к выходам (без которого не получить вычисленные значения в графе).\n",
"* Backward pass (BP) - расчёт величины градиента от выхода ко входам.\n",
"* Обновление весов, в зависимости от величины градиента. На анимации буквой $\\eta$ обозначен learning rate."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-web_dependencies/L05/backprop_animation.gif\" width=\"600\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Введение в PyTorch"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Практически вся наша работа с этого момента будет осуществляться с помощью [PyTorch](https://pytorch.org/), поэтому необходимо познакомиться с основными концептами, принципами и функциями PyTorch.\n",
"\n",
"Лучший друг в этом, конечно же, [документация](https://pytorch.org/docs/stable/index.html), однако можно привести основные моменты этой библиотеки/фреймворка:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Основная сущность — torch.Tensor"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Поскольку основная сущность, с которой мы работаем, это вектора и матрицы, то для них нужен очень мощный и функциональный класс — [`torch.Tensor`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor)\n",
"\n",
"Создание \"пустого\" тензора:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"a = torch.Tensor() "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Конструктор класса с заполнением существующими значениями:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"a = torch.tensor([1.1, 2.2, 3.2]) \n",
"a.dtype"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Явное указание типа данных:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"a = torch.tensor([1.1, 2.2, 3.2], dtype=torch.float64)\n",
"a.dtype"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Создание 2-мерного тензора, заполненного единицами (для нулей zeros)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"a = torch.ones(size=(3, 2)) \n",
"a.size()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Создание 2-мерного тензора, заполненного указанным значением\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"a = torch.full((3, 2), 5.1) \n",
"a"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Транспонирование (изменение порядка осей)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"a = a.T \n",
"a"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В библиотеке доступно огромное количество встроенных математических примитивов"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"c = torch.atan2(a[0], a[1]) \n",
"c"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Почти всё, что есть в NumPy, есть в PyTorch, например, `sum()`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"c.sum() "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Перестановка, удаление и добавление пространственных измерений:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"a = torch.zeros((2, 5, 1, 8))\n",
"print(\"Original tensor size:\\n\", a.size())\n",
"\n",
"a = a.permute(dims=(2, 0, 3, 1)) # permute dimensions\n",
"print(\"After permute tensor size:\\n\", a.size())\n",
"\n",
"a = a.squeeze() # delete dimension\n",
"print(\"After squzee tensor size:\\n\", a.size())\n",
"\n",
"a = a.unsqueeze(dim=0) # add dimension\n",
"print(\"After unsquzee tensor size:\\n\", a.size())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Преобразование torch.Tensor в NumPy-массив:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"a.numpy() "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"PyTorch позволяет тензору быть представлением (view) существующего тензора. Тензор представления использует те же базовые данные, что и его базовый тензор. Поддержка `view` позволяет избежать явного копирования данных, что позволяет нам выполнять быстрое и эффективное изменение формы, нарезку и операции с элементами."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"a = torch.rand(2, 8)\n",
"print(\"Original tensor:\\n\", a)\n",
"b = a.view(4, 4) # carefully with structured data, reshape/view can transform image to unreadable view \n",
"print(\"Tensor after view tensor:\\n\", b)\n",
"b += 1\n",
"print(\"Add 1 to tensor:\\n\", b)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Размещение тензора на GPU:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n",
"print(\"Cuda available: \", torch.cuda.is_available(), '\\n')\n",
"a = a.to(device) # tensor to gpu\n",
"b = torch.full_like(a, 2).to(device)\n",
"c = a * b # compute on gpu (more fast with parallel computing)\n",
"c"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Автоматическое вычисление градиентов"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"PyTorch умеет запоминать последовательность операций с нашими тензорами и вычислять градиент."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"x_train = torch.tensor([1., 2., 3., 4.])\n",
"y_train = torch.tensor([2., 4., 6., 8.])\n",
"\n",
"W = torch.tensor(1.0, requires_grad=True) \n",
"\n",
"print (f\"W.grad = {W.grad} (before forward pass must be 'None')\")\n",
"\n",
"y_pred = W * x_train\n",
"criterion = torch.nn.MSELoss()\n",
"MSE = criterion(y_pred, y_train)\n",
"print(f\"MSE = {MSE}\")\n",
"\n",
"# backward pass to compute gradient dMSE/dw\n",
"MSE.backward()\n",
"print (f\"W.grad = {W.grad}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Отсоединение тензора от графа вычислений (используйте при копировании тензора):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"W.detach() "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Другие интересные подмодули фреймворка"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"Torch\n",
"\n",
"* ```torch.nn``` — модуль для работы с нейронными сетями в стиле ООП\n",
"* ```torch.nn.functional``` — то же, что выше, но в функциональном стиле\n",
"* ```torch.utils.data``` — создание датасета, даталоадера\n",
"* ```torch.linalg``` — линейная алгебра\n",
"* ```torch.fft``` — преобразования Фурье\n",
"* ```torch.random``` — реализация функций, связанных со случайностью\n",
"\n",
"\n",
"[Torchvision](https://pytorch.org/vision/stable/index.html) - работа с изображениями\n",
"* ```torchvision.transforms``` — трансформации и предобработки для изображений\n",
"* ```torchvision.datasets``` — учебные датасеты\n",
"* ```torchvision.models``` — готовые модели для обработки изображений\n",
"\n",
"[Torchaudio](https://pytorch.org/audio/stable/index.html) - работа с аудио.\n",
"* ```torchaudio.transforms``` — общие методы обработки звука и извлечения признаков\n",
"* ```torchaudio.datasets``` — учебные датасеты\n",
"* ```torchaudio.models``` — готовые модели для обработки аудио\n",
"\n",
"[Torchtext](https://pytorch.org/text/stable/index.html) - работа с текстом.\n",
"* ```torchtext.transforms``` — общие методы предобработки и трансформации текста\n",
"\n",
"etc."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Backprop in PyTorch\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим алгоритм обратного распространения на примере вычисления квадрата ошибки для линейной регрессии (для простоты не будем рассматривать смещение):\n",
"\n",
"$$y=w\\cdot x, \\quad при \\;x=[1,2,3,4],\\;y=[2,4,6,8],\\;w=1$$\n",
"\n",
"В данном примере видно, что предсказанный моделью $\\hat{y}=[1,2,3,4]$ не совпадает с истинными значениями $y$, и, соответственно, квадратичная ошибка для такого примера будет: $$MSE=\\frac{1}{4}\\sum_{i=1}^4E_i^2=\\frac{1}{4}\\sum_{i=1}^4(\\hat{y}_i-y_i)^2=\\frac{1+4+9+16}{4}=7.5$$\n",
"\n",
"Градиент весов $w$ вычисляется следующим образом, в соответствии с цепным правилом:\n",
"\n",
"$$\\frac{d MSE}{d w} = \\frac{\\partial MSE}{\\partial E}\\cdot \\frac{\\partial E}{\\partial \\hat{y}}\\cdot \\frac{\\partial \\hat{y}}{\\partial w}$$\n",
"\n",
"Рассчитаем его с использованием PyTorch:\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"x_train = torch.tensor([1, 2, 3, 4], dtype=torch.float32)\n",
"y_train = torch.tensor([2, 4, 6, 8], dtype=torch.float32)\n",
"\n",
"# This is the parameter we want to optimize -> requires_grad=True\n",
"W = torch.tensor(1.0, dtype=torch.float32, requires_grad=True)\n",
"print (f\"W.grad = {W.grad} (before forward pass must be 'None')\\n\")\n",
"# forward pass to compute MSE\n",
"y_pred = W * x_train\n",
"E = y_pred - y_train\n",
"SE = E ** 2\n",
"MSE = SE.mean()\n",
"print(f\"MSE = {MSE}\")\n",
"\n",
"# backward pass to compute gradient dMSE/dw\n",
"MSE.backward()\n",
"print (f\"W.grad = {W.grad}\")\n",
"print (f\"E.grad = {E.retain_grad()}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В данном примере мы произвели следующие расчеты:\n",
"\n",
"$\\frac{\\partial MSE}{\\partial E}=\\frac{\\sum\\partial E^2}{\\partial E}=\\frac{1}{4}\\cdot2\\cdot E=\\frac{1}{2}*[-1, -2, -3, -4]=[-0.5, -1, -1.5, -2]\\quad *-поэлементное\\;умножение$\n",
"\n",
"$\\frac{\\partial E}{\\partial \\hat{y}}=\\frac{\\partial (\\hat{y}-y)}{\\partial \\hat{y}}=1$\n",
"\n",
"$\\frac{\\partial \\hat{y}}{\\partial w}=\\frac{\\partial wx}{\\partial w}=x=[1, 2, 3, 4]$\n",
"\n",
"$\\frac{d MSE}{d w} = \\frac{\\partial MSE}{\\partial E}\\cdot \\frac{\\partial E}{\\partial \\hat{y}}\\cdot \\frac{\\partial \\hat{y}}{\\partial w}=\\sum[-0.5, -1, -1.5, -2]*[1, 2, 3, 4]=-0.5-2-4.5-8=-15$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`MSE.backward()` автоматически вычисляет градиент $\\frac{dMSE}{dw}$ при указании `requires_grad=True`. \n",
"Результаты вычислений будут храниться в `W.grad`. Для всех промежуточных переменных градиенты не сохраняются, поэтому попытка обратиться, например, к `E.grad` выдает `None`. \n",
"\n",
"Также после однократного обратного прохода в целях экономии памяти граф, используемый для вычисления градиента, будет удаляться, и следующий запуск `MSE.backward()` будет выдавать ошибку:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"```\n",
"MSE.backward() # Error on second backward call\n",
"```\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Чтобы сохранить вычислительный граф, для аргумента `retain_graph` функции `backward()` нужно указать значение `True`. Также может быть полезным сохранять значения градиентов для промежуточных переменных, это делается с помощью функции `tensor.retain_grad()`. В таком случае, значения градиентов, полученные на следующих итерациях обратного распространения ошибки, будут складываться с текущими значениями градиентов.\n",
"\n",
"Градиенты переменных, для которых был указан `retain_graph=True`, сохраняются автоматически. Чтобы избежать их накопления при многократном итерировании алгоритма обратного распространения, нужно обнулять градиент на каждом шаге с помощью функции `tensor.grad.zero_()`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"x_train = torch.tensor([1, 2, 3, 4], dtype=torch.float32)\n",
"y_train = torch.tensor([2, 4, 6, 8], dtype=torch.float32)\n",
"\n",
"# This is the parameter we want to optimize -> requires_grad=True\n",
"W = torch.tensor(1.0, dtype=torch.float32, requires_grad=True)\n",
"\n",
"# forward pass to compute MSE\n",
"y_pred = W * x_train\n",
"E = y_pred - y_train\n",
"E.retain_grad() # Save grads for intermediate tensor E in memory\n",
"SE = E**2\n",
"MSE = SE.sum().div(4)\n",
"\n",
"print(\"========== Backprop 1 ==============\")\n",
"MSE.backward(retain_graph=True)\n",
"print (f\"dMSE/dE = {E.grad}\")\n",
"print (f\"dMSE/dW = {W.grad}\")\n",
"\n",
"print(\"========== Backprop 2 ==============\")\n",
"MSE.backward(retain_graph=True)\n",
"# Gradients are accumulated\n",
"print (f\"dMSE/dE = {E.grad}\")\n",
"print (f\"dMSE/dW = {W.grad}\")\n",
"\n",
"print(\"========== Backprop 3 ==============\")\n",
"W.grad.zero_() # Nullify gradients for W for the next iteration\n",
"MSE.backward(retain_graph=True)\n",
"# Gradients for W are not accumulated, but not for E\n",
"print (f\"dMSE/dE = {E.grad}\")\n",
"print (f\"dMSE/dW = {W.grad}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Итак, мы умеем вычислять градиент $\\frac{\\partial MSE}{\\partial w}$ для нашего примера. Теперь давайте с его помощью оптимизируем веса, используя алгоритм обратного распространения ошибки:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"x_train = torch.tensor([1, 2, 3, 4], dtype=torch.float32)\n",
"y_train = torch.tensor([2, 4, 6, 8], dtype=torch.float32)\n",
"\n",
"W = torch.tensor(1.0, dtype=torch.float32, requires_grad=True)\n",
"\n",
"# Define model output\n",
"def forward(x_train):\n",
" return W * x_train\n",
"\n",
"# Compute MSE loss \n",
"def criterion(y_pred, y_train):\n",
" return ((y_pred - y_train)**2).mean()\n",
"\n",
"print(f'Prediction before training: f(x) = {forward(x_train)}')\n",
"print(f'True values: y = {y_train}\\n')\n",
"\n",
"# Training\n",
"learning_rate = 0.005\n",
"num_epochs = 102\n",
"\n",
"for epoch in range(num_epochs):\n",
" # Propagate forward\n",
" y_pred = forward(x_train)\n",
"\n",
" # Compute MSE loss\n",
" MSE = criterion(y_pred, y_train)\n",
"\n",
" # Propagate backward, compute gradients\n",
" MSE.backward()\n",
"\n",
" # Update weights\n",
" with torch.no_grad(): # We don't want this step to be the part of the computational graph\n",
" W -= learning_rate * W.grad \n",
" \n",
" # Nullify gradients after updating to avoid their accumulation\n",
" W.grad.zero_()\n",
"\n",
" if epoch % 10 == 1:\n",
" print(f'epoch {epoch}: w = {W.item():.3f}, loss = {MSE.item():.8f}')\n",
"\n",
"print(f'\\nPrediction after training: f(x) = {forward(x_train)}')\n",
"print(f'True values: y = {y_train}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Видно, что наш подход позволяет оптимизировать вес $w$ регрессии из примера и таким образом добиться почти идеального предсказания нашей модели, однако в данном подходе дополнительно можно автоматизировать вычисление функции потерь и обновление параметров с учетом градиента, используя готовые функции потерь из `torch.nn` и оптимизаторы из `torch.optim`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import torch.nn as nn\n",
"\n",
"x_train = torch.tensor([1, 2, 3, 4], dtype=torch.float32)\n",
"y_train = torch.tensor([2, 4, 6, 8], dtype=torch.float32)\n",
"\n",
"W = torch.tensor(1.0, dtype=torch.float32, requires_grad=True)\n",
"\n",
"# Define model output\n",
"def forward(x_train):\n",
" return W*x_train\n",
"\n",
"\n",
"print(f'Prediction before training: f(x) = {forward(x_train)}')\n",
"print(f'True values: y = {y_train}\\n')\n",
"\n",
"# Training\n",
"learning_rate = 0.005\n",
"num_epochs = 102\n",
"\n",
"criterion = nn.MSELoss()\n",
"optimizer = torch.optim.SGD([W], lr=learning_rate)\n",
"\n",
"for epoch in range(num_epochs):\n",
" # Propagate forward\n",
" y_pred = forward(x_train)\n",
"\n",
" # Compute MSE loss\n",
" MSE = criterion(y_pred, y_train)\n",
"\n",
" # Propagate backward, compute gradients\n",
" MSE.backward()\n",
"\n",
" # Update weights\n",
" optimizer.step()\n",
"\n",
" # Nullify gradients after updating to avoid their accumulation\n",
" optimizer.zero_grad()\n",
"\n",
" if epoch % 10 == 1:\n",
" print(f'epoch {epoch}: w = {W.item():.3f}, loss = {MSE.item():.8f}')\n",
"\n",
"print(f'\\nPrediction after training: f(x) = {forward(x_train)}')\n",
"print(f'True values: y = {y_train}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Преимущества и недостатки метода"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В настоящее время метод обратного распространения ошибки фактически стал стандартом при обучении широкого спектра современных архитектур нейронных сетей. Этот метод даёт итерационное решение задачи поиска минимума функционала ошибки, последовательно подстраивая веса нейронной сети в ходе обучения. К сожалению, такой подход не гарантирует, что в ходе обучения нейронной сети мы действительно достигнем абсолютного (глобального) минимума функции потерь. Кроме того, в силу итерационной природы алгоритма, для обучения может потребоваться огромное количество циклов, что иногда приводит к необходимости проводить вычисления непрерывно в течение дней, недель или даже месяцев. Остановимся ещё на некоторых трудностях, которые могут сопровождать метод обратного распространения ошибки: "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Остановка обучения в локальном минимуме функции потерь\n",
"\n",
" На функцию потерь нейронной сети можно смотреть как на определенную поверхность в пространстве высокой размерности (размерность пространства соответствует числу весов нейронной сети). Скажем, если бы у нейронной сети было всего два веса, то такая поверхность была бы похожа на земной рельеф.\n",
"\n",
" Поверхность функции потерь во всех возникающих на практике случаях достаточно сложна и содержит высокоразмерные аналоги холмов, впадин, долин и всевозможных их комбинаций. Применяя градиентный спуск, мы фактически движемся по такой поверхности в направлении самого \"крутого\" склона вниз, начиная своё движение из какой-то фиксированной точки поверхности. Может так оказаться, что мы начали своё движение по поверхности неподалёку от небольшой впадины (локального минимума функции потерь) и, \"закатившись\" в него, не сможем больше из него выбраться, даже если совсем неподалёку в пространстве весов сети будет присутствовать значительно более \"глубокий\" минимум — все пути из неглубокого локального минимума находятся в направлении противоположном тому, согласно которому мы движемся при методе градиентного спуска. Чтобы \"выпрыгнуть\" из такого нежелательного локального минимума, может потребоваться кратковременно увеличить скорость обучения (фактически размер шага). Проблема выбора алгоритма задания оптимального шага обучения во время градиентного спуска в общем случае не решена.\n",
"\n",
" Наглядные визуализации поверхностей функций потерь настоящих нейронных сетей можно найти на странице [проекта LossLandscape](https://losslandscape.com/). Интерактивный инструмент по визуализации градиентного спуска доступен по ссылке: [LossLandscape Explorer](https://losslandscape.com/explorer)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* Паралич сети\n",
"\n",
" Пусть мы обучаем многослойную нейронную сеть, в которой в качестве функции активации нейронов используются сигмоидальные функции (логистическая функция или гиперболический тангенс). Если мы допустим слишком сильное обновление весов у большого числа нейронов, чего можно легко добиться выбором слишком большой величины скорости (шага) обучения, то дальнейшее обучение сети фактически может остановиться. Дело в том, что в области больших по модулю значений аргумента сигмоидальной функции, она практически не изменяется, а значит, её производная слабо отличается от нуля. Так как обновление весов на следующем шаге градиентного спуска будет пропорционально значению производной функции активации, то оно также будет близко к нулю. Такой переход нейронов в \"насыщение\" может быть уже необратимым, и методу градиентного спуска потребуется неограниченно много времени для возвращения сети в исходное \"рабочее\" состояние."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Функции потерь (loss functions)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Предположим, у нас есть нейронная сеть с некоторыми весами. Прежде всего мы должны понять, насколько она точна, то есть насколько наши ожидания соответствуют результату работы нейронной сети. Мы подали на вход нейронной сети изображение, сигналы прошли через наши слои и функции активации вперёд **(forward propagation)**, и на выходе мы имеем некоторый ответ. Как его оценить? Насколько он точен?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Для оценки соответствия полученного результата ожидаемому, используют функцию потерь (loss function). Значение функции потерь даёт количественную оценку величины такого соответствия."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Функция потерь в нейронной сети принимает два аргумента:\n",
"* вектор значений, который мы считаем априорно верным;\n",
"* вектор значений конечных выходов модели, который должен соответствовать априорно верному.\n",
"\n",
"Важно заметить, что для успешного обучения модели методом градиентного спуска мы должны потребовать от функции потерь дифференцируемости и ограниченности снизу.\n",
"\n",
"[Документация по функциям потерь в PyTorch](https://pytorch.org/docs/stable/nn.html#loss-functions)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Mean squared error"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Mean Squared Error (MSE) — это средняя квадратическая ошибка. Данная функция потерь очень популярная, поскольку она проста для понимания и реализации, и в целом работает довольно хорошо. Чтобы рассчитать MSE, нужно взять разницу между предсказаниями вашей модели и эталонными значениями, возвести в квадрат и затем усреднить по всему набору данных (в случае обучения по мини-батчам - по размеру батча).\n",
"Результат всегда положительный, независимо от знака предсказанных и истинных значений, и идеальное значение равно 0.0"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$MSE=\\frac{1}{n}\\sum_{i=1}^n(\\hat{Y_i} - Y_i)^2$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[MSE Loss в PyTorch](https://pytorch.org/docs/stable/generated/torch.nn.MSELoss.html#torch.nn.MSELoss):\n",
"```python\n",
"torch.nn.MSELoss()\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"criterion = nn.MSELoss()\n",
"\n",
"input_values = torch.Tensor([0.5, -0.25, 0.75])\n",
"print(f'input_values: {input_values}')\n",
"\n",
"target = torch.Tensor([1, 0.25, 0.25])\n",
"print(f'target: {target}')\n",
"\n",
"loss_mse = criterion(input_values, target)\n",
"print(f'loss_mse: {loss_mse}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* **Преимущество:** Использование MSE в качестве функции потерь даёт основания ожидать, что обученная с ней модель не имеет сильных \"выбросов\" в величине ошибки. Любой большой выброс в невязке $|\\hat {Y_i} - Y_i| \\ggg 0 $ при вычислении MSE был бы возведён в квадрат и дал бы вовсе огромный вклад в итоговое значение функции потерь.\n",
"\n",
"\n",
"* **Недостаток:** Как логично следует из описанного выше преимущества MSE, данная функция потерь в первую очередь сильно штрафует модель за наличие выбросов в предсказаниях. Для ряда практически важных задач, тем не менее, важнее оказывается наиболее высокая точность работы на абсолютном большинстве входных примеров, нежели отсутствие одиночных выбросов."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Mean Absolute Error "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Средняя абсолютная ошибка (MAE) — это величина, которая измеряет среднюю по всем образцам величину невязки $|\\hat{Y_i} - Y_i|$. Несмотря на то, что определение этой функции потерь похоже на MSE (MSE Loss можно назвать $L_2$ ошибкой, а MAE в этом смысле можно назвать $L_1$ ошибкой), средняя абсолютная ошибка имеет существенно другие свойства. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$MAE=\\frac{1}{n}\\sum_{i=1}^n|\\hat{Y_i} - Y_i|$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[MAE Loss в PyTorch](https://pytorch.org/docs/stable/generated/torch.nn.L1Loss.html#torch.nn.L1Loss):\n",
"```python\n",
"torch.nn.L1Loss()\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"criterion = nn.L1Loss()\n",
"\n",
"input_values = torch.Tensor([0.5, -0.25, 0.75])\n",
"print(f'input_values: {input_values}')\n",
"\n",
"target = torch.Tensor([1, 0.25, 0.25])\n",
"print(f'target: {target}')\n",
"\n",
"loss_mae = criterion(input_values, target)\n",
"print(f'loss_mae: {loss_mae}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* **Преимущество:** Поскольку при вычислении MAE вычисляется абсолютное значение ошибки, данная метрика не придаёт такого большого значения \"выбросам\", как MSE — все ошибки учитываются равнозначно в единой линейной шкале.\n",
"\n",
"\n",
"* **Недостаток:** Недостаток применения MAE в качестве функции потерь при обучении модели напрямую вытекает из преимуществ. Обученная с MAE модель может показывать хорошие (или даже отличные) результаты в большинстве случаев, но на отдельных примерах может допускать большую ошибку. Если специфика решаемой задачи не позволяет нам пренебречь такими одиночными большими выбросами, то следует воздержаться от применения данной функции потерь."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Cross-Entropy"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Кросс-энтропия (перекрёстная энтропия) является одной из наиболее часто применимых в практике обучения нейронных сетей функцией потерь. С точки зрения теории информации, кросс-энтропия между двумя вероятностными распределениями $p$ и $q$ над одним и тем же вероятностным пространством определяет среднее количество информации (измеренное в количестве бит), необходимое для идентификации одиночного события из вероятностного пространства, если информация о таком событии записывается в виде оптимизированного под \"оценочное\" распределение $q$, по сравнению с использованием \"истинного\" распределения $p$.\n",
"\n",
"Интуитивно кросс-энтропию можно понимать в качестве меры близости \"оценочного\" распределения $q$ к истинному распределению $p$. На практике элементами распределения $p$ является набор ground true значений из обучающей выборки, а элементы оценочного распределения $q$ являются выводами используемой нами модели.\n",
"\n",
"Формально кросс-энтропию можно записать в виде:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$ H(p,q) = -E_p[\\log q]$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Где:\n",
"* $E_p$ — оператор математического ожидания относительно распределения $p$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Однако чаще кросс-энтропию определяют с помощью энтропии и расстояния Кульбака-Лейблера:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$H(p,q) = H(p) +D_{KL}(p||q)$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В случае нейронных сетей, где вероятности представлены дискретными выходами, формула превращается в:\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$H(p,q)=-\\sum_xp(x)\\log q(x)$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/probabiliry_cross_entropy.png\" width=\"800\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Поскольку чаще всего кросс-энтропию используют после Softmax, то в готовых реализациях Softmax объединяют с кросс-энтропией:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/softmax_with_cross_entropy.png\" width=\"700\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[Cross-Entropy Loss в PyTorch](https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html#torch.nn.CrossEntropyLoss):\n",
"```python\n",
"torch.nn.CrossEntropyLoss()\n",
"```\n",
"\n",
"Обратите внимание, что Cross-Entropy Loss в PyTorch уже включает в себя Softmax, и принимает в качестве выхода модели логиты. Поэтому используя данную фукцию потерь, использовать использовать на последнем слое нейронной сети Softmax **не нужно**."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"criterion = nn.CrossEntropyLoss()\n",
"\n",
"input_values = torch.rand(3, 3)\n",
"print(f'input_values: {input_values}')\n",
"\n",
"target = torch.empty(3, dtype=torch.long).random_(3)\n",
"print(f'target: {target}')\n",
"\n",
"loss_ce = criterion(input_values, target)\n",
"print(f'loss_ce: {loss_ce}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* **Преимущества:** Важное свойство кросс-энтропии — возможность работать с весами для классов. А значит и возможность применения этой функции потерь при работе с несбалансированным датасетом.\n",
"* **Недостатки:** Вычислительная сложность выше, чем у MSE или MAE."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Binary Cross-Entropy"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В частном случае, когда количество классов равно двум и их можно закодировать одним числом: 0 — для первого класса, и 1 для второго, то формулу Cross-Entropy Loss можно адаптировать для этого случая:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$H_p(q)=-\\frac{1}{N}\\sum_{i=1}^N y_i\\cdot log(p(y_i))+(1-y_i)\\cdot log(1-p(y_i))$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[Binary Cross-Entropy Loss в PyTorch](https://pytorch.org/docs/stable/generated/torch.nn.BCELoss.html#torch.nn.BCELoss):\n",
"\n",
"```python\n",
"torch.nn.BCELoss()\n",
"```\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Важной особенностью BCELoss является то, что она ждёт одно число выхода сети и одно число как верный результат. Тут используется не one-hot кодировка для двух классов, а **одно число 0 — первый класс, 1 — второй класс.**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"criterion = nn.BCELoss()\n",
"\n",
"input_values = torch.rand(1)\n",
"print(f'input_values: {input_values}')\n",
"\n",
"target = torch.empty(1).random_(2)\n",
"print(f'target: {target}')\n",
"\n",
"loss_bce = criterion(input_values, target)\n",
"print(f'loss_bce: {loss_bce}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Если классы \"абсолютно полностью\" не совпали, то возникает ситуация взятия логарифма от 0, а он не определён и стремится к бесконечности, поэтому берётся \"обрезанная бесконечность\" равная 100.\n",
"\n",
"Далее, если сэмплов несколько, то по умолчанию берётся среднее по семплам. См. аргумент `reduction`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"criterion = nn.BCELoss()\n",
"\n",
"input_values = torch.ones((5))\n",
"print(f'input_values: {input_values}')\n",
"\n",
"target = torch.zeros(5)\n",
"print(f'target: {target}')\n",
"\n",
"loss_bce = criterion(input_values, target)\n",
"print(f'loss_bce: {loss_bce}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Итоги\n",
"\n",
"Кросс-энтропия предпочтительнее для задач *классификации*, так от модели требуется предсказание *вероятности класса* при известной априорной вероятности. Средняя квадратическая и средняя абсолютная ошибки предпочтительны для задач *регрессии*, когда от модели требуется предсказание произвольных вещественных чисел. \n",
"\n",
"И то, и другое можно рассматривать как оценки максимального правдоподобия, просто с различными предположениями о зависимой переменной.\n",
"\n",
"Здесь были рассмотрены лишь наиболее общие и часто применимые функции потрерь для основополагающих задач машинного обучения: классификации и регрессии. Однако на практике могут возникать случаи, когда от исследователя или разработчика нейронной сети требуется сконструировать свою собственную функцию потерь под свою собственную задачу. В PyTorch, помимо рассмотренных, есть реализация и других функций потерь для других задач. Также свою \"кастомную\" функцию потерь можно написать самостоятельно.\n",
"\n",
"[Обзор функций потерь в PyTorch с примером написания своей собственной функции (custom loss function)](https://neptune.ai/blog/pytorch-loss-functions)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Функции активации"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Минимальным функциональным элементом нейронной сети является одиночный нейрон. В нейроне осуществляется две операции:\n",
"1. Вычисляется взвешенная и смещенная сумма его входов\n",
"$$ s=\\sum_{i=1}^n w_i \\cdot x_i+b=WX+b,$$\n",
"где $W$ обозначает вектор весов, $X$ обозначает вектор входных значений, а $b$ задаёт величину смещения.\n",
"1. К получившейся величине применяется некоторая нелинейная функция, называемая функцией активации\n",
"$$ y = f(s)$$\n",
"\n",
"\n",
"Взвешенная и смещенная сумма входов $s$ может принимать произвольное значение на вещественной прямой: $s \\in \\mathbb{R}$. Данное значение передается в функцию активации, которая, как правило, обладает другим множеством возможных значений."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/neurons_output.png\" width=\"900\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Идея применения функций активации в структуре искусственных нейронных сетей обусловлена биологической аналогией. Известно, что в биологических нейронных сетях имеется аналог нелинейной функции активации: существует пороговый потенциал, только после достижения которого происходит возбуждение (активация) нейрона и, как следствие, распространение сигнала далее по нейронной сети. \n",
"\n",
"\n",
"Именно таким простейшим образом ведёт себя пороговая функция активации, которая использовалась при построении первых искусственных нейронных сетей — перцептронов:\n",
"\n",
"\n",
"$f(x) =\n",
"\\begin{cases}\n",
"0, &\\text{$x<b$} \\\\ \n",
"1, &\\text{$x\\geq b$}\n",
"\\end{cases}\n",
"$\n",
"\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/threshold_function_plot.png\" width=\"300\">\n",
"\n",
"\n",
"Построенная с пороговой функцией активации нейронная сеть обладает ключевым недостатком, не позволяющим фактически использовать данную функцию активации на практике. В силу того, что производная функции активации тривиальна почти всюду на числовой прямой: \n",
"\n",
"$f'(x) =\n",
"\\begin{cases}\n",
"0, &\\text{$x\\neq b$} \\\\ \n",
"?, &\\text{$x= b$}\n",
"\\end{cases}\n",
"$\n",
"\n",
"не представляется возможным использовать метод градиентного спуска для оптимизации параметров нейронной сети."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Свойства функций активации"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Функции активации должны обладать следующими свойствами:\n",
"\n",
"* **Нелинейность:** функция активации необходима для введения нелинейности в нейронные сети. Если функция активации не применяется, выходной сигнал становится простой линейной функцией. Нейронная сеть без нелинейностей будет действовать как линейная модель с ограниченной способностью к обучению:\n",
"$$\\hat{y}=NN(X,W_1,...,W_n)=X\\cdot W_1\\cdot ...\\cdot W_n=X\\cdot W$$ \n",
"Только нелинейные функции активации позволяют нейронным сетям решать задачи аппроксимации нелинейных функций:\n",
"$$\\hat{y}=NN(X,W_1,...,W_n)=\\sigma(...\\sigma(X\\cdot W_1)...\\cdot W_n)\\neq X\\cdot W$$\n",
"\n",
"* **Дифференцируемость:** функции активации должны быть способными пропускать градиент, чтобы было возможно оптимизировать параметры сети градиентными методами, в частности использовать алгоритм обратного распространения ошибки."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Различные функции активации"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим наиболее популярные функции активации и обсудим их преимущества и недостатки."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-web_dependencies/L05/popular_activation_functions.png\" width=\"700\">\n",
"\n",
"<center><p><em>Source: <a href=\"https://arxiv.org/pdf/1911.05187.pdf\">AI in Pursuit of Happiness, Finding\n",
"Only Sadness: Multi-Modal Facial\n",
"Emotion Recognition Challenge</a></p> </em></center>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### **Логистическая функция**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Логистическая (сигмоидальная) функция — используется в задачах бинарной классификации, в основном после выхода последнего нейрона. Позволяет определить вероятность принадлежности к одному из двух классов (0 или 1)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$\\sigma(x)=\\frac{1}{1+e^{-x}}$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/activation_function_sigmoid.png\" width=\"1000\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Примечательным свойством логистической функции является то, что ее производная выражается через саму функцию. Это значит, что зная значение функции в точке, вычислить значение производной в этой точке очень легко:\n",
"\n",
"$$ \\frac{d}{dx}\\sigma(x) = \\frac{d}{dx}(1+e^{-x})^{-1} = \\frac{e^{-x}}{(1+e^{-x})^{2}} = \\frac{1}{1+e^{-x}} \\cdot \\frac{1+e^{-x}-1}{1+e^{-x}} = \\sigma(x)\\cdot(1-\\sigma(x))$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В отличие от пороговой функции активации, где у нейрона было всего два состояния: \"активирован\" или \"не активирован\", с логистической функцией для нейрона возможны значения \"активирован на 50%\", \"активирован на 20%\" и так далее. Если активированы несколько нейронов, можно найти нейрон с наибольшим значением активации.\n",
"\n",
"Так как существуют промежуточные значения на выходе нейрона, **процесс обучения проходит более гладко и быстро**, а вероятность появления нескольких полностью активированных нейронов во время тренировки снижается по сравнению с пороговой функцией активации (хотя это зависит от того, что вы обучаете и на каких данных)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[Сигмоидальная функция активации в PyTorch](https://pytorch.org/docs/stable/generated/torch.nn.Sigmoid.html):\n",
"```python\n",
"torch.nn.Sigmoid()\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"activation = nn.Sigmoid()\n",
"input_values = torch.randn(5) * 5\n",
"activation_sig = activation(input_values)\n",
"print(f'input_values: {input_values}\\nactivation_sig: {activation_sig}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Сигмоида выглядит гладкой и подобна пороговой функции.\n",
"\n",
"**Достоинства:**\n",
"\n",
"Во-первых, сигмоида нелинейна по своей природе, а комбинация таких функций производит тоже нелинейную функцию. Поэтому мы можем конструировать многослойные сети.\n",
"\n",
"Еще одно достоинство такой функции — она гладкая, следовательно, улучшается гладкость градиента, в отличие от ступенчатой функции."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"**Недостатки:**\n",
"\n",
"Насыщение сигмоиды приводит к затуханию градиентов. Крайне нежелательное свойство сигмоиды заключается в том, что при насыщении функции с той или иной стороны (0 или 1), градиент на этих участках становится близок к нулю. Напомним, что в процессе обратного распространения ошибки данный (локальный) градиент умножается на общий градиент. Следовательно, если локальный градиент очень мал, он фактически обнуляет общий градиент. В результате, сигнал почти не будет проходить через нейрон к его весам. Кроме того, следует быть очень осторожным при инициализации весов сигмоидных нейронов, чтобы предотвратить насыщение. Например, если исходные веса имеют слишком большие значения, большинство нейронов перейдет в состояние насыщения, в результате чего сеть будет плохо обучаться."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Выход сигмоиды не центрирован относительно нуля. Это свойство является нежелательным, поскольку нейроны в последующих слоях будут получать значения, которые не центрированы относительно нуля, что оказывает влияние на динамику градиентного спуска. Если значения, поступающие в нейрон, всегда положительны (например, $x > 0$ поэлементно в $f = wx + b$), тогда в процессе обратного распространения ошибки все градиенты весов $w$ будут либо положительны, либо отрицательны (в зависимости от градиента всего выражения $f$). Это может привести к нежелательной зигзагообразной динамике обновлений весов. Однако следует отметить, что, когда эти градиенты суммируются по батчу, итоговое обновление весов может иметь различные знаки, что отчасти нивелирует описанный недостаток. Таким образом, отсутствие центрирования является неудобством, но имеет менее серьезные последствия по сравнению с проблемой насыщения."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### **Гиперболический тангенс**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Гиперболический тангенс схож с логистической функцией. Он определяется следующей формулой:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$tanh(x)=\\frac{e^x - e^{-x}}{e^x+e^{-x}}$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Также гиперболический тангенс может быть выражен через логистическую функцию:\n",
"\n",
"$$tanh(x) = 2\\cdot\\sigma(2x)-1$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/activation_function_tanh.png\" width=\"1000\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Производная гиперболического тангенса также [выражается через саму функцию](https://socratic.org/questions/what-is-the-derivative-of-tanh-x):\n",
"\n",
"$$ \\frac{d}{dx}tanh(x)=1-tanh^2(x)$$\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Гиперболический тангенс симметричен относительно нуля и может принимать как положительные, так и отрицательные значения. Данное свойство гиперболического тангенса оказывается важным, в частности, при построении рекуррентных нейронных сетей. При использовании в рекуррентных сетях, получаемые на выходе $\\tanh (x)$ положительные или отрицательные значения могут не только увеличивать величину скрытого состояния в ячейках памяти, но и уменьшать их. Подробнее с устройством рекуррентных нейронных сетей мы познакомимся в последующих лекциях нашего курса.\n",
"\n",
"**Достоинства**: В силу схожего определения, гиперболический тангенс обладает основными достоинствами описанной выше логистической функции. Кроме того, множество значений данной функции активации симметрично относительно нуля $[-1,1]$. Использование гиперболического тангенса в качестве функции активации хорошо подходит для последовательного соединения полносвязных слоёв нейронной сети.\n",
"\n",
"**Недостатки**: Производная гиперболического тангенса по виду аналогична производной логистической функции, следовательно, при использовании гиперболического тангенса в качестве функции активации мы также можем столкнуться с проблемой затухания градиентов в области насыщения функции.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[Гиперболический тангенс в PyTorch](https://pytorch.org/docs/stable/generated/torch.nn.Tanh.html):\n",
"```python\n",
"torch.nn.Tanh()\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"activation = nn.Tanh()\n",
"input_values = torch.tensor([11.1529, 4.3029, 0.5081, -3.8456, -1.9058])\n",
"activation_tanh = activation(input_values)\n",
"print(f'input_values: {input_values}\\nactivation_tanh: {activation_tanh}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### **ReLU**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Часто на практике применяется функция активации ReLU. Значение данной функции равно нулю для всех отрицательных входных значений и равно с входному значению, если оно неотрицательно. Название ReLU (Rectified Linear Unit), \"выпрямитель\", связана с электротехнической аналогией — график вольт-амерной характеристики идеального выпрямительного диода похож на график функции ReLU.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$ReLU(x)=max(0,x)$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/activation_function_relu.png\" width=\"1000\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Производная ReLU:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$\\frac{d}{dx}ReLU(x) =\n",
"\\begin{cases}\n",
"\\frac{d}{dx}0, &\\text{$x<0$} \\\\ \n",
"\\frac{d}{dx}x, &\\text{$x\\geq0$}\n",
"\\end{cases}=\n",
"\\begin{cases}\n",
"0, &\\text{$x<0$} \\\\ \n",
"1, &\\text{$x\\geq0$}\n",
"\\end{cases}\n",
"$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[ReLU в PyTorch](https://pytorch.org/docs/stable/generated/torch.nn.ReLU.html):\n",
"```python\n",
"torch.nn.ReLU()\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"activation = nn.ReLU()\n",
"input_values = torch.randn(5)\n",
"activation_relu = activation(input_values)\n",
"print(f'input_values: {input_values}\\nactivation_relu: {activation_relu}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим положительные и отрицательные стороны ReLU.\n",
"\n",
"**Достоинства:** \n",
"\n",
"Функция ReLU не требует проведения вычислений в вещественной арифметике, как того требуют логистическая функция или гиперболический тангенс. Кроме того, производная функции ReLU является кусочно-постоянной функцией и также может быть вычислена крайне эффективно. Это приводит к тому, что количество необходимых вычислительных ресурсов для обучения нейронной сети с использованием ReLU оказывается значительно ниже, нежели чем при использовании рассмотренных выше логистической функции или гиперболического тангенса. Необходимо также отметить, что использование ReLU не приводит к эффекту насыщения нейронов."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"**Недостатки:**\n",
"\n",
"Иногда при использовании ReLU в качестве функции активации мы можем столкнуться с нежелательным эффектом отключения (\"умирания\") отдельных нейронов. Механизм данного явления связан с возможностью получения на выходе функции активации нулевого значения при широком диапазоне входных сигналов — любая отрицательная линейная комбинация входных значений с весами нейрона будет преобразована ReLU в ноль. Если при текущем обновлении весов нейрона изменение может оказаться слишком большим (например, при выборе слишком высокой скорости обучения), новая конфигурация весов нейрона будет при любых входных значениях приводить к отрицательной линейной комбинации и, как следствие, тождественно равной нулю активации рассматриваемого нейрона. Такой нейрон также тождественно обратит в ноль и проходящий через него локальный градиент при обучении сети методом обратного распространения ошибки, что сделает практически невозможным возвращение нейрона в \"рабочее\" состояние. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### **Leaky ReLU**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Leaky ReLU (ReLU с «утечкой», название также обусловлено электротехнической аналогией) является простейшей модификацией описанной выше ReLU, призванной исправить проблему \"умирания\" отдельных нейронов. В отличие от ReLU, данная функция не равна константе $0$ при всех отрицательных входных значениях, а реализует в этой области линейную зависимость с небольшим угловым коэффициентом (например, с угловым коэффициентом $10^{-2}$). "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$LeakyReLU(x)=max(0.01x,x)$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/activation_function_leaky_relu.png\" width=\"1000\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Производная Leaky ReLU:\n",
"\n",
"$$\\frac{d}{dx}LeakyReLU(x)=\\frac{d}{dx}max(0.01x,x)=\\begin{cases}\n",
"\\frac{d}{dx}0.01x, &\\text{$x<0$} \\\\ \n",
"\\frac{d}{dx}x, &\\text{$x\\geq0$}\n",
"\\end{cases}=\n",
"\\begin{cases}\n",
"0.01, &\\text{$x<0$} \\\\ \n",
"1, &\\text{$x\\geq0$}\n",
"\\end{cases}$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[Leaky ReLU в PyTorch](https://pytorch.org/docs/stable/generated/torch.nn.LeakyReLU.html):\n",
"```python\n",
"torch.nn.LeakyReLU()\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"activation = nn.LeakyReLU(0.01)\n",
"input_values = torch.randn(5)\n",
"activation_lrelu = activation(input_values)\n",
"print(f'input_values: {input_values}\\nactivation_lrelu: {activation_lrelu}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Достоинства**: Сохраняет достоинства ReLU, при этом не страдает от проблемы \"умирания\" \n",
"\n",
"**Недостатки**: Некоторые исследователи сообщают об успешном применении данной функции активации, но результаты не всегда стабильны."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### **GELU (Gaussian Error Linear Unit)**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Функция активации, используемая в трансформерах: Google BERT и OpenAI GPT-2."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"$$GELU(x)=xP(X\\leq x)=x\\Phi(x)=x\\cdot \\frac{1}{2}[1+erf(\\frac{x}{\\sqrt{2}})]$$\n",
"\n",
"$$erf(x)=\\frac{2}{\\sqrt{\\pi}}\\int_0^xe^{-t^2}dt$$\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"На практике GELU может быть приблизительно вычислена так:\n",
"$$GELU(x)\\approx 0.5x(1+tanh[\\sqrt{2/\\pi}(x+0.044715x^3)])$$ \n",
"\n",
"или \n",
"\n",
"$$GELU(x) \\approx x\\cdot \\sigma(1.702x)$$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/activation_function_gelu.png\" width=\"1000\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Достоинства**: State-of-the-art функция активации в задачах NLP"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"activation = nn.GELU()\n",
"input_values = torch.randn(5) * 5\n",
"activation_gelu = activation(input_values)\n",
"print(f'input_values: {input_values}\\nactivation_gelu: {activation_gelu}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[GELU в PyTorch](https://pytorch.org/docs/stable/generated/torch.nn.GELU.html):\n",
"```python\n",
"torch.nn.GELU()\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Визуализация функций активации:\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"<img src =\"https://edunet.kea.su/repo/EduNet-content/L05/out/animated_activation_functions.gif\" width=\"600\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[How Activation Functions Work in Deep Learning](https://www.kdnuggets.com/2022/06/activation-functions-work-deep-learning.html)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Углубление в PyTorch"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Dataset и DataLoader\n",
"Код для предварительной обработки данных может быть запутанным и его может быть сложно поддерживать. В идеале, мы бы хотели, чтобы код, относящийся к набору данных, был отделен от кода для обучения модели для его лучшей читаемости и понимаемости. \n",
"\n",
"PyTorch предоставляет два базовых класса для работы с данными: `torch.utils.data.DataLoader` и `torch.utils.data.Dataset`, которые позволяют работать как со встроенными наборами данных, так и с вашими собственными данными. \n",
"\n",
"`Dataset` хранит в себе объекты (samles, сэмплы) — например, изображения, и соответсвующие им метки (labels). \n",
"\n",
"`DataLoader` представляет из себя итерируемый объект — обертку над `Dataset`-ом, и позволяет получить простой доступ к объектам из набора данных.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Библиотеки из семейства PyTorch предоставляют ряд предзагруженных наборов данных (например, таких как MNIST), которые релизованы как дочерние классы от `torch.utils.data.Dataset` и несут в себе функции, специфичные для конкретных данных. Они могут быть использованы как бенчмарк для отладки и оценки вашей модели. Вы можете найти их здесь: [Image Datasets](https://pytorch.org/vision/stable/datasets.html), [Text Datasets](https://pytorch.org/text/stable/datasets.html), and [Audio Datasets](https://pytorch.org/audio/stable/datasets.html)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Загрузка набора данных\n",
"Рассмотрим пример того, как загрузить набор данных MNIST, которые содержится в `torchvision`. MNIST содержит 60&nbsp;000 изображений для обучения и 10&nbsp;000 изображений для теста, размеченных на 10 классов — по числу цифр. Каждый пример представляет собой изображение размером 28×28 пикселей в оттенках серого. Каждое изображение имеет метку класса — то, какая цифра на нем изображена.\n",
"\n",
"Загрузим MNIST со следующими параметрами:\n",
"* `root` — это путь, куда будут скачаны данные,\n",
"* `train` определяет, скачивать обучающую или тестовую часть набора данных,\n",
"* `download=True` позволяет скачать данные из интернета, если их нет в пути `root`,\n",
"* `transform` определяет преобразования, которые нужно сделать с данными."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"from torch.utils.data import Dataset\n",
"from torchvision import datasets\n",
"from torchvision.transforms import ToTensor\n",
"import matplotlib.pyplot as plt\n",
"\n",
"train_data = datasets.MNIST(root=\"./MNIST\", train=True, download=True, transform=ToTensor())\n",
"\n",
"test_data = datasets.MNIST(root=\"./MNIST\", train=False, download=True, transform=ToTensor())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Итерирование по `Dataset` и визуализация данных\n",
"\n",
"Можно индексировать `Dataset` вручную, как список или массив: `train_data[index]`. При таком обращении мы получим кортеж `(sample, label)`. Воспользуемся matplotlib, чтобы отобразить первые 10 изображений из обучающего множества."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"num_imgs_to_visualize = 10\n",
"\n",
"figure = plt.figure(figsize=(20, 20))\n",
"\n",
"for i in range(num_imgs_to_visualize):\n",
" # here we indexing the Dataset-object \"as is\" and get a tuple (img, label)\n",
" img, label = train_data[i] \n",
" \n",
" figure.add_subplot(1, num_imgs_to_visualize, i+1)\n",
" plt.imshow(img.squeeze(), cmap=\"gray\")\n",
" plt.title(label)\n",
" plt.axis(\"off\")\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Подготовка данных к обучению с помощью DataLoader"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`Dataset` возвращает по одной паре \"пример — метка\" за раз. При обучении моделей мы обычно хотим передавать обекты в виде мини-батчей, перемешивая данные на каждой эпохе для уменьшения переобучения.\n",
"\n",
"`DataLoader` — это итерируемый объект, который позволяет нам получать такие мини-батчи.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from torch.utils.data import DataLoader\n",
"\n",
"train_dataloader = DataLoader(train_data, batch_size=8, shuffle=True)\n",
"test_dataloader = DataLoader(test_data, batch_size=8, shuffle=False)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Итерирование по `DataLoader`\n",
"\n",
"Мы загрузили датасет в `DataLader` и теперь можем проходиться по нему по мере необходимости. Каждая итерация ниже будет возвращать мини-батч в виде кортежа тензоров `(samples, labels)`, содержащих `batch_size=8` объектов и меток соответственно.\n",
"Так как мы установили для `train_dataloader` параметр `shuffle=True`, когда мы пройдемся по всем батчам, данные перемешаются."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# get next batch\n",
"imgs, labels = next(iter(train_dataloader))\n",
"\n",
"print(f\"Images batch shape: {imgs.size()}\")\n",
"print(f\"Labels batch shape: {labels.size()}\")\n",
"\n",
"print(\"The first sample in the batch:\")\n",
"img = imgs[0].squeeze()\n",
"label = labels[0].item()\n",
"\n",
"plt.imshow(img, cmap=\"gray\")\n",
"plt.title(label)\n",
"plt.axis(\"off\")\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Пример простой сети на датасете MNIST"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Загрузим датасет:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"import torchvision\n",
"from torch.utils.data import DataLoader\n",
"from torchvision.datasets import MNIST\n",
"from IPython.display import clear_output\n",
"\n",
"transform = torchvision.transforms.Compose(\n",
" [torchvision.transforms.ToTensor(),\n",
" torchvision.transforms.Normalize((0.5), (0.5))])\n",
"\n",
"train_set = MNIST(root='./MNIST', train=True, download=True, transform=transform)\n",
"test_set = MNIST(root='./MNIST', train=False, download=True, transform=transform)\n",
"\n",
"batch_size = 64\n",
"train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=2)\n",
"test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=2)\n",
"\n",
"clear_output()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Набор данных MNIST (англ. Modified National Institute of Standards and Technology) представляет собой модельный набор данных с изображениями рукописных цифр, который часто используется для обучения и оценки качества работы ML моделей классификации изображений.\n",
"\n",
"Датасет состоит из $60\\ 000$ тренировочных и $10\\ 000$ тестовых изображений. Все изображения имеют одинаковый квадратный размер $28 \\times 28$ пикселей и единственный цветовой канал. Рукописные изображения цифр имеют равный размер и располагаются в центре кадра. Кроме того, все изображения имеют одинаковую яркость. Всего датасет содержит 10 классов изображений: цифры от $0$ до $9$."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"x_train = train_set.data.numpy() # images\n",
"y_train = train_set.targets.numpy() # labels\n",
"labels_names = list(map(str, range(10)))\n",
"plt.figure(figsize = (20.0, 20.0)) \n",
"for i in range(10): # for all classes (0 to 9)\n",
" label_indexes = np.where(y_train == i)[0] # get indexes for each class \n",
" index = np.random.choice(label_indexes)\n",
" img = x_train[index]\n",
"\n",
" plt.subplot(1, 10, i + 1) \n",
" plt.title(labels_names[i]) \n",
" plt.axis(\"off\") \n",
" plt.imshow(img,cmap='gray') "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В PyTorch для создания нейронных сетей требуется отнаследоваться от класса `nn.Module` и переопределить метод `forward`, в который подаются входные данные, и который возвращает выход сети."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[PyTorch документация по nn.Module](https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В класс мы добавляем две переменные, два слоя Linear. Linear — это слой, позволяющий, умножить веса на входной вектор и добавить смещение. Первый параметр — размер входного вектора, второй — размер выходного.\n",
"\n",
"В методе `forward` мы указываем последовательность применения операций для получения результата. Сначала изменим представление входного вектора, чтобы от изменения `batch_size` у нас ничего не сломалось.\n",
"\n",
"Далее идёт первый слой, после него функция активации ReLU и второй слой, возвращающий вектор длиной 10, содержащий логиты, означающие принадлежность к одному из классов."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import torch.nn as nn\n",
"\n",
"class Net(nn.Module):\n",
" def __init__(self):\n",
" super().__init__()\n",
" self.fc1 = nn.Linear(28*28, 128) \n",
" self.fc2 = nn.Linear(128, 10)\n",
" self.activation = nn.ReLU()\n",
"\n",
" def forward(self, x): # Called inside __call__ method\n",
" x = x.view(-1, 28*28) # \"reshape\" image to vector\n",
" x = self.fc1(x)\n",
" x = self.activation(x)\n",
" x = self.fc2(x)\n",
" return x"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Наследование от `nn.Module` позволяет обращаться к параметрам сети через `model.parameters()`, а так же упростить много других вещей. **Это критически удобно для обучения сетей**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Второй вариант последовательного выполнения слоёв сети — [`nn.Sequential`](https://pytorch.org/docs/stable/generated/torch.nn.Sequential.html) — объединение слоёв, выход одного слоя передается на вход в следующий."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class Net(nn.Module):\n",
" def __init__(self):\n",
" super().__init__()\n",
" self.layers = nn.Sequential(\n",
" nn.Linear(28*28, 128),\n",
" nn.ReLU(),\n",
" nn.Linear(128, 10)\n",
" )\n",
" def forward(self, x):\n",
" x = x.view(-1, 28*28)\n",
" x = self.layers(x)\n",
" return x"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"model = Net()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В качестве функции потерь воспользуемся кросс-энтропией:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"criterion = nn.CrossEntropyLoss()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Определим оптимизатор для нашей сети. Оптимизатор, в библиотеках для нейронных сетей — это сущность, осуществляющая градиентный спуск. Подробнее об оптимизаторах будет рассказано в дальнейших лекциях, а сейчас воспользуемся базовым стохастическим градиентным спуском (Stochastic gradient descent, SGD)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"optimizer = torch.optim.SGD(model.parameters(), lr=0.001)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Обучим сеть десять эпох:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from tqdm.notebook import trange\n",
"\n",
"num_epochs = 10\n",
"loss_hist = [] # for plotting\n",
"epochs = trange(num_epochs)\n",
"for epoch in epochs:\n",
" hist_loss = 0\n",
" for _, batch in enumerate(train_loader, 0): # get batch\n",
" # parse batch \n",
" imgs, labels = batch\n",
" # sets the gradients of all optimized tensors to zero.\n",
" optimizer.zero_grad()\n",
" # get outputs\n",
" pred = model(imgs)\n",
" # calculate loss\n",
" loss = criterion(pred, labels)\n",
" # calculate gradients\n",
" loss.backward()\n",
" # performs a single optimization step (parameter update)\n",
" optimizer.step()\n",
" hist_loss += loss.item()\n",
" loss_hist.append(hist_loss / len(train_loader))\n",
" epochs.set_description(f\"Epoch={epoch} loss={loss_hist[epoch]:.4}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.figure(figsize = (10, 5))\n",
"plt.plot(range(num_epochs), loss_hist)\n",
"plt.xlabel(\"Epochs\", fontsize=15)\n",
"plt.ylabel(\"Loss\", fontsize=15)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Давайте посчитаем accuracy.\n",
"\n",
"Помните, что accuracy является частным случаем метрики качества, поэтому она не реализована в PyTorch и с ней нужно быть аккуратней. Функция ниже подходит только для задачи классификации при one-hot энкодинге, когда классов минимум 2."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def calculate_accuracy(model, data_loader):\n",
" correct, total = 0, 0 \n",
" with torch.no_grad(): \n",
" for batch in data_loader: # get batch\n",
" imgs, labels = batch # parse batch\n",
" pred = model(imgs) # get output\n",
" _, predicted = torch.max(pred.data, 1) # get predicted class\n",
" total += labels.size(0) # all examples\n",
" correct += (predicted == labels).sum().item() # correct predictions \n",
" return correct / total "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"acc_train = round(calculate_accuracy(model, train_loader), 3)\n",
"print(f\"Accuracy train = {acc_train}\")\n",
"\n",
"acc_test = calculate_accuracy(model, test_loader)\n",
"print(f\"Accuracy test = {acc_test}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Поэтапно пропустим тестовые изображения через модель и визуализируем результат"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# get batch\n",
"imgs, labels = next(iter(test_loader))\n",
"print('imgs shape: ', imgs.shape)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# get output\n",
"pred = model(imgs)\n",
"print('pred shape: ', pred.shape)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# First sample in prediction batch\n",
"pred[0]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Calculate probabilities\n",
"nn.Softmax(dim=0)(pred[0].detach())"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# remove axis\n",
"imgs = torch.reshape(imgs, (64, 28, 28))\n",
"print('imgs shape(after reshape): ', imgs.shape)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# take 10 first images\n",
"imgs = imgs[:10]\n",
"print('imgs shape: ', imgs.shape)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pred = pred[:10].detach()\n",
"print('Prediction(1 sample):\\n', pred[0])\n",
"digits = np.argmax(pred.numpy(), axis=1)\n",
"print('Predicted class: ', digits[0])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Визуализируем изображения, подпишем предсказанное и истинное значение:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.figure(figsize = (25.0, 25.0))\n",
"for i in range(10):\n",
" img = imgs[i]\n",
"\n",
" plt.subplot(1, 10, i + 1)\n",
" plt.title('pred: ' + str(digits[i]) + ' real: '+str(labels[i].numpy())) # predicted and real values\n",
" plt.axis(\"off\")\n",
" plt.imshow(img.numpy(),cmap='gray') "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
" <font size=\"6\">Ссылки:</font>\n",
"\n",
"[StatSoft. Радиальная базисная функция](http://statsoft.ru/home/textbook/modules/stneunet.html#radial)\n",
"\n",
"[Важность функции потери в машинном обучении](https://towardsdatascience.com/importance-of-loss-function-in-machine-learning-eddaaec69519#:~:text=At%20its%20core%2C%20a%20loss,to%20minimize%20the%20loss%20function.)\n",
"\n",
"[Understanding Categorical Cross-Entropy Loss, Binary Cross-Entropy Loss, Softmax Loss, Logistic Loss, Focal Loss and all those confusing names](https://gombru.github.io/2018/05/23/cross_entropy_loss/)\n",
"\n",
"[Функции активации нейросети: сигмоида, линейная, ступенчатая, ReLU, tahn](https://neurohive.io/ru/osnovy-data-science/activation-functions/)\n",
"\n",
"[Объясненные современные функции активации: GELU, SELU, ELU, ReLU и другие](https://www.kdnuggets.com/2022/06/activation-functions-work-deep-learning.html)"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 0
}