{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
" Нейронные сети"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Ограничения Линейного классификатора"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Вспомним материал лекции №2:\n",
"\n",
"Мы обучали линейный классификатор на датасете CIFAR-10\n",
"\n",
"\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": [
"
"
]
},
{
"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",
"
"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Все описанные выше сложности обобщенно можно назвать **внутриклассовой вариативностью**: мы можем приписывать к одному классу объекты, которые допускают широкий спектр определения. Так, например, мы обобщаем классом \"кошка\" кошек различных пород, размеров и возрастов. \"Хороший\" алгоритм классификации должен быть устойчив к внутриклассовой вариативности и верно распознавать все возможные варианты объектов."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Один из подходов к решению этой проблемы — модифицировать модель таким образом, чтобы на у нее внутри было не по одному шаблону на каждый класс, а по несколько (скажем, вместо 100 шаблонов вместо 10 при десяти классах). Тогда бы Ямодель имела возможность запоминать разные объекты одного класса и далее использовать эти промежуточные шаблоны для разбиения объектов на классы."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"
"
]
},
{
"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",
"
\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",
"
"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"
\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"И оно соответствует одному слою искусственной нейронной сети (за исключением функции активации)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"
"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Многослойные сети"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"По мере развития мощности компьютеров, теоретической базы, появления больших датасетов и метода обратного распространения ошибки, появилась возможность строить более сложные сети — многослойные нейронные сети или же в современном понимании просто **нейронные сети**."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Пример **полносвязной (fully-connected network)** нейронной сети с двумя скрытыми слоями:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"
"
]
},
{
"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": [
"
"
]
},
{
"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": [
"
"
]
},
{
"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": [
"
"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Рассмотрим простой пример. На вход нейрона подаётся входное значение $x$, умноженное на вес $w$. После применения функции активации, в зависимости от веса, при всевозможных значениях входа мы можем получить следующие графики при $w$ равном $0.5$, $1$ и $2$:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"
\n",
"\n",
"
Source: Immersion in Machine learning
Source: Immersion in Machine learning