diff --git a/README.md b/README.md
index 9c05f88..fc89ece 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-# Sample-DL-Repo
+# Tarasov Nikita
diff --git a/Tarasov_hw3.ipynb b/Tarasov_hw3.ipynb
new file mode 100644
index 0000000..d55c895
--- /dev/null
+++ b/Tarasov_hw3.ipynb
@@ -0,0 +1,1354 @@
+{
+ "nbformat": 4,
+ "nbformat_minor": 0,
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.7"
+ },
+ "colab": {
+ "name": "Tarasov_hw3.ipynb",
+ "provenance": [],
+ "collapsed_sections": []
+ }
+ },
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "ucUIoKyJdAxb"
+ },
+ "source": [
+ "# Пишем свой фреймворк"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "o2DkIzfVdAxd"
+ },
+ "source": [
+ "# только numpy, только хардкор\n",
+ "import numpy as np"
+ ],
+ "execution_count": 1,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "zL1UwDwNdAxi"
+ },
+ "source": [
+ "Мотивация: конечным пользователям вашего фреймворка не хочется думать, как они работают слои внутри. Им просто хочется объявить последовательность элементарных операций над входными данными, а о градиентах и прочем матане пусть позаботится сам фреймворк.\n",
+ "\n",
+ "**Module** — это абстрактный класс, от которого будут наследоваться слои нашей нейронной сети. Абстрактные классы нужны, чтобы можно было реализовывать не все методы, а только переопределить некоторые. Все в лучших традициях ООП.\n",
+ "\n",
+ "Модуль — это такая чёрная коробка, которая\n",
+ "1. Умеет принимать какие-то входные данные $X$ и возращать какие-то выходные данные $Y$ (`forward`)\n",
+ "2. Возможно, имеет какие-то параметры, которые можно изменять, (`parameters`, `grad_parameters`)\n",
+ "3. Будучи встроенной в вычислительный граф, умеет по градиенту относительно своих выходных значений вычислять градиент относительно входных данных, а также собственных параметров (`backward`)\n",
+ "4. Умеет переключаться в режимы обучения и инференса, если они отличаются (`train`, `eval`)\n",
+ "\n",
+ "Теперь поподробнее."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "BPQOD218dAxj"
+ },
+ "source": [
+ "## Входные данные\n",
+ "\n",
+ "Современные нейросети оптимизируют различными вариантами стохастического градиентного спуска, и мы тоже будем его использовать. Его отличие от обычного в том, что на каждом шаге мы не считаем градиент на всем датасете (это было бы слишком долго), а оцениваем его, усреднив градиенты на его случайно выбранной малой части, которую называют батчем (`batch`). Если батч формировать случайно, и если его размер достаточно большой, то мы можем быстро получить немного шумную, но приемлемую для нас оценку градиента, и не прогонять через сеть все миллионы примеров ради одного маленького шага. Эта интуиция ограничивает размер батча сверху."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "4--xRXAkdAxk"
+ },
+ "source": [
+ "Математик бы принял время прогона одного примера по всей сети за константу и пришел бы к выводу, что нужно считать по одному примеру и делать каждый раз один шаг, но маленький. Это верное заключение, но в реальности, если увеличить размер батча в $k$ раз, то он будет работать не в $k$ раз дольше, а намного меньше.\n",
+ "\n",
+ "Самая долгая операция в большинстве нейросетей — это перемножение матриц. Начиная с каких-то размеров для их перемножения имеет использовать алгоритм Штрассена, который работает уже быстрее, чем линейно. Проведём небольшой вычислительный эксперимент:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "tfA1Fp7kdAxl",
+ "outputId": "e5aaa5df-6f34-48de-9e55-d672b1198b2b",
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ }
+ },
+ "source": [
+ "A = np.random.randn(256, 2000)\n",
+ "B = np.random.randn(2000, 800)\n",
+ "\n",
+ "# помножить каждый вектор-строку на B и сконкатенировать\n",
+ "%time C = np.stack(np.dot(A[i].T, B) for i in range(256))\n",
+ "\n",
+ "# это то же самое, что использовать одно-большое матричное умножение\n",
+ "%time C = np.dot(A, B)"
+ ],
+ "execution_count": 2,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stderr",
+ "text": [
+ "/usr/local/lib/python3.7/dist-packages/IPython/core/magic.py:188: FutureWarning: arrays to stack must be passed as a \"sequence\" type such as list or tuple. Support for non-sequence iterables such as generators is deprecated as of NumPy 1.16 and will raise an error in the future.\n",
+ " call = lambda f, *a, **k: f(*a, **k)\n"
+ ]
+ },
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "CPU times: user 319 ms, sys: 33.1 ms, total: 352 ms\n",
+ "Wall time: 193 ms\n",
+ "CPU times: user 59.3 ms, sys: 12.9 ms, total: 72.2 ms\n",
+ "Wall time: 40.2 ms\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "Xoj5u-YHdAxp"
+ },
+ "source": [
+ "Такая чисто вычислительная причина ограничивает размер батча снизу. На практике, в большинстве случаев оптимальный размер батча — несколько сотен. В случае с CPU это несколько десятков, потому что выгода от распараллеливания вычислений не такая сильная.\n",
+ "\n",
+ "Вообще, почти все наши слои будут работать с векторами независимо, но из-за вычилсительных причин мы будем объединять их в матрицы. Вообще, более сложные нейросети работают с тензорами. «Тензор» это, вообще говоря, сложный математический объект, но в DL этот термин используется просто в занчении «многомерный массив». Например, картинки — это четырехмерный тензор: `[batch, channel, x, y]`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "yeECJUJvdAxq"
+ },
+ "source": [
+ "### Forward\n",
+ "\n",
+ "Эта функция просто принимает тензор (`numpy.ndarray`) и возвращает какой-то другой, над которым применили соответствующие операции.\n",
+ "\n",
+ "Важный нюанс: нам позже для реализации `backward` почти всегда будет нужно сохранять где-нибудь выход `forward` (это создает очень большую нагрузку на память при обучении; [в принципе это можно и не делать](https://arxiv.org/pdf/1604.06174.pdf), но так проще). Условимся сохранять его в `self.output`, сразу после того, как посчитали."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "s0i3gvWMdAxq"
+ },
+ "source": [
+ "### Параметры\n",
+ "\n",
+ "Параметр модели — это что-то, что можно поодгонять, чтобы функция потерь стала меньше. Он должен быть доступен оптимизатору, а оптимизатору не обязательно знать, как всё у слоя все внутри работает. Ему нужны просто градиенты — насколько ему нужно подвинуть параметры сети, чтобы стало лучше.\n",
+ "\n",
+ "Общаться с ним мы будем посредством двух функций: `params` и `grad_params`. Обе возвращают списки из тензоров — значения параметров и их посчитанных градиентов (см. `backward`) соответственно. Питон делает shallow copy, поэтому у оптимизатора так есть доступ на их изменение."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "jcZHrwjZdAxr"
+ },
+ "source": [
+ "### Backward\n",
+ "\n",
+ "После того, как мы в вычислительном графе все последовательно посчитали и дошли до функции потерь, нам надо подогнать параметры так, чтобы на тех же данных при повторном прогоне она стала меньше — иными словами, нам надо сделать шаг против градиента функции потерь относительно параметров сети.\n",
+ "\n",
+ "Посчитать эти градиенты — нетривиальная задача. Мы могли бы рассмотреть каждый параметр по отдельности и как-нибудь посчитать градиент для него. Но это очень долго — параметров в современных сетях бывает по несколько миллионов.\n",
+ "\n",
+ "Вместо этого мы применим трюк, основанный на формуле для производной сложной функции:\n",
+ "\n",
+ "$$ f(g(x))' = f'(g(x)) \\cdot g'(x) $$\n",
+ "\n",
+ "Представьте, что часть сети от параметра до выхода — это всего две последовательно выполненные функции: $g$ и $f$. Тогда, согласно формуле, нам для этого параметра достаточно посчитать и перемножить две величины — $g'(x)$ (производная текущего слоя) и f'(g(x)) (производная относительно выхода текущего слоя).\n",
+ "\n",
+ "Какие-то другие параметры могли тоже зависеть от производной относительно выхода. и мы получаем выигрыш за счет того, что считаем её только один раз и запоминаем. Можно сказать, что мы применяем таким образом динамическое программирование на вычислительном графе, чтобы посчитать градиенты относительно всех его параметров.\n",
+ "\n",
+ "Обратный прогон (`backward`) определяется для каждого слоя и нужен как раз для подсчета градиентов, имея градиент относительно своих выходных значений (аналог $f'(g(x)))$.\n",
+ "\n",
+ "Он должен делать две вещи:\n",
+ "\n",
+ "1. Посчитать градиент относительно собственных параметров.\n",
+ "2. Посчитать и вернуть градиент относительно своих входных данных.\n",
+ "\n",
+ "Для лучшего понимания рассмотрите пример с `Linear` и `ReLU`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "UQAOtCLddAxs"
+ },
+ "source": [
+ "### train / eval\n",
+ "\n",
+ "Некоторые слои ведут себя по-разному во время обучечния и предсказания (`inference`). Обычно, это связано с разного вида регуляризацией — например, так ведут себя `BatchNorm` и `Dropout`.\n",
+ "\n",
+ "По сути, для таких слоев нужно просто написать два разных `forward`-а для обучения и инференса."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "oBVpDMN6dAxs"
+ },
+ "source": [
+ "class Module():\n",
+ " def __init__(self):\n",
+ " self._train = True\n",
+ " \n",
+ " def forward(self, input):\n",
+ " raise NotImplementedError\n",
+ "\n",
+ " def backward(self,input, grad_output):\n",
+ " raise NotImplementedError\n",
+ " \n",
+ " def parameters(self):\n",
+ " 'Возвращает список собственных параметров.'\n",
+ " return []\n",
+ " \n",
+ " def grad_parameters(self):\n",
+ " 'Возвращает список тензоров-градиентов для своих параметров.'\n",
+ " return []\n",
+ " \n",
+ " def train(self):\n",
+ " self._train = True\n",
+ " \n",
+ " def eval(self):\n",
+ " self._train = False"
+ ],
+ "execution_count": 3,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "s-137oXtdAxu"
+ },
+ "source": [
+ "Это **абстрактный класс** — от него наследуются другие слои, в которых эти функции будут реализованы."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "TV9xeNAndAxv"
+ },
+ "source": [
+ "# Sequential"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "J-mgMM1MdAxw"
+ },
+ "source": [
+ "**Sequential** будет оборачивать список модулей и выполнять их последовательно.\n",
+ "\n",
+ "Это своего рода контейнер, внутри которого есть какой-то пайплайн.\n",
+ "\n",
+ "Можно даже засовывать один Sequential внутри другого."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "qdWwOprDdAxw"
+ },
+ "source": [
+ "Многие не знают, но в питоне почти всегда для итерирования используется не **deep copy**, а **shallow copy**. Это делается для экономии памяти."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "0O968yWrdAxx"
+ },
+ "source": [
+ "class Sequential(Module):\n",
+ " def __init__ (self, *layers):\n",
+ " super().__init__()\n",
+ " self.layers = layers\n",
+ "\n",
+ " def forward(self, input):\n",
+ " \"\"\"\n",
+ " Прогоните данные последовательно по всем слоям:\n",
+ " \n",
+ " y[0] = layers[0].forward(input)\n",
+ " y[1] = layers[1].forward(y_0)\n",
+ " ...\n",
+ " output = module[n-1].forward(y[n-2]) \n",
+ " \n",
+ " Это должен быть просто небольшой цикл: for layer in layers...\n",
+ " \n",
+ " Хранить выводы ещё раз не надо: они сохраняются внутри слоев после forward.\n",
+ " \"\"\"\n",
+ "\n",
+ " for layer in self.layers:\n",
+ " input = layer.forward(input)\n",
+ "\n",
+ " self.output = input\n",
+ " return self.output\n",
+ "\n",
+ " def backward(self, input, grad_output):\n",
+ " \"\"\"\n",
+ " Backward -- это как forward, только наоборот. (с)\n",
+ " \n",
+ " Предназначение backward:\n",
+ " 1. посчитать посчитать градиенты для собственных параметров\n",
+ " 2. передать градиент относительно своего входа\n",
+ " \n",
+ " О своих параметрах модули сами позаботятся. Нам же нужно позаботиться о передачи градиента.\n",
+ " \n",
+ " g[n-1] = layers[n-1].backward(y[n-2], grad_output)\n",
+ " g[n-2] = layers[n-2].backward(y[n-3], g[n-1])\n",
+ " ...\n",
+ " g[1] = layers[1].backward(y[0], g[2]) \n",
+ " grad_input = layers[0].backward(input, g[1])\n",
+ " \n",
+ " Тут цикл будет уже чуть посложнее.\n",
+ " \"\"\"\n",
+ " \n",
+ " for i in range(len(self.layers)-1, 0, -1):\n",
+ " grad_output = self.layers[i].backward(self.layers[i-1].output, grad_output)\n",
+ " \n",
+ " grad_input = self.layers[0].backward(input, grad_output)\n",
+ " \n",
+ " return grad_input\n",
+ " \n",
+ " def parameters(self):\n",
+ " 'Можно просто сконкатенировать все параметры в один список.'\n",
+ " res = []\n",
+ " for l in self.layers:\n",
+ " res += l.parameters()\n",
+ " return res\n",
+ " \n",
+ " def grad_parameters(self):\n",
+ " 'Можно просто сконкатенировать все градиенты в один список.'\n",
+ " res = []\n",
+ " for l in self.layers:\n",
+ " res += l.grad_parameters()\n",
+ " return res\n",
+ " \n",
+ " def train(self):\n",
+ " for layer in self.layers:\n",
+ " layer.train()\n",
+ " \n",
+ " def eval(self):\n",
+ " for layer in self.layers:\n",
+ " layer.eval()"
+ ],
+ "execution_count": 4,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "50R6jFr8dAxy"
+ },
+ "source": [
+ "# Слои"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "guFtN_3FdAxz"
+ },
+ "source": [
+ "Приступим к реализации содержательной части — самих слоев.\n",
+ "\n",
+ "На вход всех слоев будет подаваться матрица размера `batch_size` $\\times$ `n_features` (см. описание `forward`)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "jdBP7xZzdAxz"
+ },
+ "source": [
+ "Начнем с основного: линейный слой, он же fully-conected.\n",
+ "\n",
+ "$$ Y = X W + b $$\n",
+ "\n",
+ "Правильнее его называть афинным: после матричного умножения добавляется вектор $b$.\n",
+ "\n",
+ "`forward` у него трививальный, а `backward` уже сложнее: нужно посчитать градиенты относительно трёх вещей:\n",
+ "1. Входных данных. Автор добродушен и спалит вам ответ, а вам нужно его доказать: $\\nabla X = W^T (\\nabla Y)$.\n",
+ "2. Матрица весов $W$. Тут нужно подумать, как каждый вес влияет на каждое выходное значение, и выразить ваши мысли линейной алгеброй.\n",
+ "3. Вектор $b$. С ним всё будет просто.\n",
+ "\n",
+ "Не забудьте, что `grad_params` должен иметь такие же размерности, как и соответствующие параметры."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "4uEjKIasdAx3"
+ },
+ "source": [
+ "class Linear(Module):\n",
+ " def __init__(self, dim_in, dim_out):\n",
+ " super().__init__()\n",
+ " stdv = 1./np.sqrt(dim_in)\n",
+ " self.W = np.random.uniform(-stdv, stdv, size=(dim_in, dim_out))\n",
+ " self.b = np.random.uniform(-stdv, stdv, size=dim_out)\n",
+ " \n",
+ " def forward(self, input):\n",
+ " self.output = np.dot(input, self.W) + self.b\n",
+ " return self.output\n",
+ " \n",
+ " def backward(self, input, grad_output):\n",
+ " self.grad_b = np.mean(grad_output, axis=0)\n",
+ " self.grad_W = np.dot(input.T, grad_output)\n",
+ " grad_input = np.dot(grad_output, self.W.T)\n",
+ " return grad_input\n",
+ " \n",
+ " def parameters(self):\n",
+ " return [self.W, self.b]\n",
+ " \n",
+ " def grad_parameters(self):\n",
+ " return [self.grad_W, self.grad_b]"
+ ],
+ "execution_count": 5,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "-jLyEmMEdAx7"
+ },
+ "source": [
+ "## Функции активации"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "H0ci6studAx7"
+ },
+ "source": [
+ "**ReLU** — одна из самых простых функций активации:\n",
+ "\n",
+ "$$\n",
+ "ReLU(x)=\n",
+ "\\begin{cases}\n",
+ "x, & x > 0\\\\\n",
+ "0, & x \\leq 0\\\\\n",
+ "\\end{cases}\n",
+ "$$\n",
+ "\n",
+ "`ReLU` это очень простой слой, поэтому автору не жалко её реализовать его за вас:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "HrayJEANdAx7"
+ },
+ "source": [
+ "class ReLU(Module):\n",
+ " def __init__(self):\n",
+ " super().__init__()\n",
+ " \n",
+ " def forward(self, input):\n",
+ " self.output = np.maximum(input, 0)\n",
+ " return self.output\n",
+ " \n",
+ " def backward(self, input, grad_output):\n",
+ " grad_input = np.multiply(grad_output, input > 0)\n",
+ " return grad_input"
+ ],
+ "execution_count": 6,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "xUTaMFaWdAx8"
+ },
+ "source": [
+ "У ReLU есть проблема — у него бесполезная нулевая производная при $x < 0$.\n",
+ "\n",
+ "[**Leaky Rectified Linear Unit**](http://en.wikipedia.org/wiki%2FRectifier_%28neural_networks%29%23Leaky_ReLUs) — это его модифицированная версия, имеющая в отрицательных координатах не нулевой градиент, а просто помноженный на маленькую константу `slope`.\n",
+ "\n",
+ "$$\n",
+ "LeakyReLU_k(x)=\n",
+ "\\begin{cases}\n",
+ "x, & x > 0\\\\\n",
+ "kx, & x \\leq 0\\\\\n",
+ "\\end{cases}\n",
+ "$$\n",
+ "\n",
+ "При `slope` = 0 он превращается в обычный `ReLU`. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "OMWPFjxldAx8"
+ },
+ "source": [
+ "class LeakyReLU(Module):\n",
+ " def __init__(self, slope=0.03):\n",
+ " super().__init__()\n",
+ " \n",
+ " self.slope = slope\n",
+ " \n",
+ " def forward(self, input):\n",
+ " self.output = np.maximum(input, input*self.slope)\n",
+ " return self.output\n",
+ " \n",
+ " def backward(self, input, grad_output):\n",
+ " grad_input = (input > 0) + self.slope * (input <= 0)\n",
+ " return grad_input"
+ ],
+ "execution_count": 7,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "EorDi-TMdAx9"
+ },
+ "source": [
+ "**Сигмоида** определяется формулой $\\sigma(x) = \\frac{1}{1+e^{-x}}$.\n",
+ "\n",
+ "\n",
+ "\n",
+ "Когда-то она была самой часто используемой функции активации, потому что имела логичную вероятностную интерпретацию (вероятность наличия какой-то фичи), но потом перестали, потому что на очень больших или маленьких значениях её производные почти нулевые (см. проблема затухающего градиента).\n",
+ "\n",
+ "Также используют [гипреболический тангенс](https://ru.wikipedia.org/wiki/%D0%93%D0%B8%D0%BF%D0%B5%D1%80%D0%B1%D0%BE%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8), который на самом деле просто сигмоида, отнормированная так, чтобы значения были в $[-1, 1]$: $tanh(x) = 2 \\sigma(x) - 1$. Мы его отдельно реализовывать не будем.\n",
+ "\n",
+ "Давайте посчитаем её производную:\n",
+ "\n",
+ "$$\n",
+ "\\begin{align}\n",
+ "\\sigma'(x) &= (\\frac{1}{1+e^{-x}})'\n",
+ "\\\\ &= \\frac{e^{-x}}{(1+e^{-x})^2}\n",
+ "\\\\ &= \\frac{1+e^{-x}-1}{(1+e^{-x})^2}\n",
+ "\\\\ &= \\frac{1+e^{-x}}{(1+e^{-x})^2} - \\frac{1}{(1+e^{-x})^2}\n",
+ "\\\\ &= \\frac{1}{1+e^{-x}} - \\frac{1}{(1+e^{-x})^2}\n",
+ "\\\\ &= \\sigma(x) - \\sigma(x)^2\n",
+ "\\\\ &= \\sigma(x)(1 - \\sigma(x))\n",
+ "\\end{align}\n",
+ "$$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "LUag-tekdAx-"
+ },
+ "source": [
+ "class Sigmoid(Module):\n",
+ " def __init__(self, slope=0.03):\n",
+ " super().__init__()\n",
+ "\n",
+ " def forward(self, input):\n",
+ " self.output = 1 / (1 + np.exp(-input))\n",
+ " return self.output\n",
+ " \n",
+ " def backward(self, input, grad_output):\n",
+ " grad_input = self.output*(1 - self.output)*grad_output\n",
+ " return grad_input"
+ ],
+ "execution_count": 8,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "kBhjcx0XdAx_"
+ },
+ "source": [
+ "**Софтмакс** определяется так:\n",
+ "\n",
+ "$$ \\sigma(x)_k = \\frac{e^{x_k}}{\\sum_{i=1}^n e^{x_i} }$$\n",
+ "\n",
+ "Можно заметить, что сигмоида — это частный случай софтмакса. Его можно интерпретировать как вероятностное распределение: его выходы положительны и суммируются в единицу. Поэтому его используют как последний слой для классификации.\n",
+ "\n",
+ "Софтмакс — самый сложный с точки зрения написания `backward`. Как и все остальное, оно считается в 5 строчек кода, но [вывести их трудно](https://deepnotes.io/softmax-crossentropy). "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "QS-6BIe6dAx_"
+ },
+ "source": [
+ "class SoftMax(Module):\n",
+ " def __init__(self):\n",
+ " super().__init__()\n",
+ "\n",
+ " def forward(self, input):\n",
+ " self.output = np.subtract(input, input.max(axis=1, keepdims=True)) \n",
+ " esum = np.sum(np.exp(self.output), axis=1, keepdims=True)\n",
+ " self.probs = np.array(np.exp(self.output)) / esum\n",
+ " self.output = self.probs\n",
+ " return self.output\n",
+ " \n",
+ " def backward(self, input, grad_output):\n",
+ " grad_input = []\n",
+ " for b in range(self.probs.shape[0]):\n",
+ " eye = np.eye(self.probs.shape[1])\n",
+ " prob_matr = np.repeat(self.probs[b].reshape(1,-1), self.probs[b].shape[0], axis=0)\n",
+ " res = np.dot(grad_output[b].reshape(1,-1), prob_matr.T*(eye-prob_matr))\n",
+ " grad_input.append(res)\n",
+ " grad_input = np.array(grad_input).reshape(-1, self.probs[b].shape[0])\n",
+ " return grad_input"
+ ],
+ "execution_count": 9,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "h-5W8O3zdAyA"
+ },
+ "source": [
+ "## Регуляризация"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "V5bmlefZdAyA"
+ },
+ "source": [
+ "Самый популярный регуляризатор в нейросетях — [**дропаут**](https://www.cs.toronto.edu/~hinton/absps/JMLRdropout.pdf). Идея простая: просто помножим поэлементно входные данные на случайную бинарную маску того же размера, как и сами данные. Сгенерировать маску можно через `np.random.binomial`.\n",
+ "\n",
+ "Дропаута обычно хватает как единственного регуляризатора. Если вы заметите, что сеть оверфитится — просто добавьте его побольше.\n",
+ "\n",
+ "**У дропаута разное поведение в режимах `train` и `eval`**. При `eval` он не должен делать ничего, а в `train` вместо применения маски нужно ещё домножить вход на $p$, чтобы скомпенсировать дропаут при обучении (так математическое ожидание значений будет такое же, как на трейне)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "9nZsHZUDdAyA",
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 70
+ },
+ "outputId": "482e37ac-9eb0-4093-ff9d-3b3bd6dff930"
+ },
+ "source": [
+ "'''\n",
+ "class Dropout(Module):\n",
+ " def __init__(self, p=0.5):\n",
+ " super().__init__()\n",
+ " \n",
+ " self.p = p\n",
+ " self.mask = None\n",
+ " \n",
+ " def forward(self, input):\n",
+ " if self._train:\n",
+ " mask = # ...\n",
+ " self.output = # ...\n",
+ " else:\n",
+ " # ...\n",
+ " return self.output\n",
+ " \n",
+ " def backward(self, input, grad_output):\n",
+ " if self._train:\n",
+ " # ...\n",
+ " else:\n",
+ " # ...\n",
+ " return grad_input\n",
+ "'''"
+ ],
+ "execution_count": 10,
+ "outputs": [
+ {
+ "output_type": "execute_result",
+ "data": {
+ "text/plain": [
+ "'\\nclass Dropout(Module):\\n def __init__(self, p=0.5):\\n super().__init__()\\n \\n self.p = p\\n self.mask = None\\n \\n def forward(self, input):\\n if self._train:\\n mask = # ...\\n self.output = # ...\\n else:\\n # ...\\n return self.output\\n \\n def backward(self, input, grad_output):\\n if self._train:\\n # ...\\n else:\\n # ...\\n return grad_input\\n'"
+ ],
+ "application/vnd.google.colaboratory.intrinsic+json": {
+ "type": "string"
+ }
+ },
+ "metadata": {},
+ "execution_count": 10
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "r0YNMOOIdAyB"
+ },
+ "source": [
+ "`BatchNorm` -- относительно современный слой, сильно улучшающий сходимость. Всё, что он делает -- это нормирует свои входные значения так, что на выходе получаются значения со средним 0 и дисперсией 1.\n",
+ "\n",
+ "\n",
+ "\n",
+ "Почитать про вывод градиента для него можно тут: https://wiseodd.github.io/techblog/2016/07/04/batchnorm/\n",
+ "\n",
+ "BatchNorm тоже по-разному ведёт себя при обучении и инференсе. Во время инференса он использует в качестве оценки среднего и дисперсии свои экспоненциально усреднённые исторические значения. Это связано с тем, что батч может быть маленьким, и оценки среднего и дисперсии будут неточными (при батче размера 1 дисперсия вообще будет нулевая, и нам в алгоритме нужно будет делить на ноль)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "Bkl3--6RdAyB",
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 70
+ },
+ "outputId": "1bbbbae8-d8c5-4d72-ff59-94c102695f64"
+ },
+ "source": [
+ "'''\n",
+ "class BatchNorm(Module):\n",
+ " def __init__(self, num_features, gamma):\n",
+ " super().__init__()\n",
+ " self.gamma = gamma\n",
+ " self.mu = # ...\n",
+ " self.sigma = # ...\n",
+ " \n",
+ " def forward(self, input):\n",
+ " if self._train:\n",
+ " # ...\n",
+ " else:\n",
+ " # ...\n",
+ " return self.output\n",
+ " \n",
+ " def backward(self, input, grad_output):\n",
+ " if self._train:\n",
+ " # ...\n",
+ " else:\n",
+ " # ...\n",
+ " return grad_input\n",
+ "'''"
+ ],
+ "execution_count": 11,
+ "outputs": [
+ {
+ "output_type": "execute_result",
+ "data": {
+ "text/plain": [
+ "'\\nclass BatchNorm(Module):\\n def __init__(self, num_features, gamma):\\n super().__init__()\\n self.gamma = gamma\\n self.mu = # ...\\n self.sigma = # ...\\n \\n def forward(self, input):\\n if self._train:\\n # ...\\n else:\\n # ...\\n return self.output\\n \\n def backward(self, input, grad_output):\\n if self._train:\\n # ...\\n else:\\n # ...\\n return grad_input\\n'"
+ ],
+ "application/vnd.google.colaboratory.intrinsic+json": {
+ "type": "string"
+ }
+ },
+ "metadata": {},
+ "execution_count": 11
+ }
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "IO_okIAhdAyC"
+ },
+ "source": [
+ "## Критерии"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "5jZL4UjNdAyC"
+ },
+ "source": [
+ "Критерии — это специальные функции, которые меряют качество, имея реальные данные и предсказанные. Все критерии возвращают скаляр — одно число, усреднённое значение метрики по всему батчу.\n",
+ "\n",
+ "По сути это тоже модули, но мы всё равно создадим для них отдельный класс, потому что у них нет `train` / `eval`, а `backward` не требует `grad_output` — эта вершина и так конечная в вычислительном графе. Также нам не понадобится сохранять для них `output`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "COX7VXtUdAyC"
+ },
+ "source": [
+ "class Criterion(): \n",
+ " def forward(self, input, target):\n",
+ " raise NotImplementedError\n",
+ "\n",
+ " def backward(self, input, target):\n",
+ " raise NotImplementedError"
+ ],
+ "execution_count": 12,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "YF1c6q1adAyD"
+ },
+ "source": [
+ "В качестве примера реализуем среднюю квадратичную ошибку (`MSE`).\n",
+ "\n",
+ "Обратите внимание, что в критериях мы делим итоговое число на размер батча — мы не хотим, чтобы функция потерь зависела от количества примеров."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "RLZXaG3OdAyD"
+ },
+ "source": [
+ "class MSE(Criterion):\n",
+ " def forward(self, input, target):\n",
+ " batch_size = input.shape[0]\n",
+ " self.output = np.sum(np.power(input - target, 2)) / batch_size\n",
+ " return self.output\n",
+ " \n",
+ " def backward(self, input, target):\n",
+ " self.grad_output = (input - target) * 2 / input.shape[0]\n",
+ " return self.grad_output"
+ ],
+ "execution_count": 13,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "tBCAlI7CdAyE"
+ },
+ "source": [
+ "Ваша задача посложнее: вам нужно реализовать кроссэнтропию — это стандартная функция потерь для классификации. Тут можно почитать про вывод её градиентов, а также софтмакса: https://deepnotes.io/softmax-crossentropy\n",
+ "\n",
+ "Напоминаем интуицию за принципом максимального правдоподобия: мы максимизируем произведение предсказанных вероятностей реально случившихся событий $ L = \\prod p_i $.\n",
+ "\n",
+ "Произведение оптимизировать не очень удобно, и поэтому мы возьмём логарифм (любой, ведь все логарифмы отличаются в константу раз) и будем вместо него максимизировать сумму:\n",
+ "\n",
+ "$$ \\log L = \\log \\prod p_i = \\sum \\log p_i $$\n",
+ "\n",
+ "Эту штуку называют кроссэнтропией. Такое название пошло из теории информации, но нам пока знать это не надо.\n",
+ "\n",
+ "Для удобноства вместо чисел — от 0 до 9 — будем использовать вектора размера 10, где будет стоять единица в нужном месте (такое кодирование называется one-hot)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "id": "3ckg7SdddAyF"
+ },
+ "source": [
+ "class CrossEntropy(Criterion):\n",
+ " def __init__(self):\n",
+ " super().__init__()\n",
+ " \n",
+ " def forward(self, input, target): \n",
+ " # чтобы нигде не было взятий логарифма от нуля:\n",
+ " eps = 1e-9\n",
+ " self.input_clamp = np.clip(input, eps, 1 - eps)\n",
+ " eye = np.eye(self.input_clamp.shape[1])\n",
+ " self.ohe = np.squeeze(eye[target], axis = 1)\n",
+ " self.res = np.log(self.input_clamp)*self.ohe\n",
+ " self.output = -np.sum(self.res, axis=1) / target.shape[0]\n",
+ " return self.output\n",
+ "\n",
+ " def backward(self, input, target):\n",
+ " self.grad_output = (self.input_clamp - self.ohe)/target.shape[0]\n",
+ " return self.grad_output"
+ ],
+ "execution_count": 14,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "%matplotlib inline"
+ ],
+ "metadata": {
+ "id": "9gCXyj1WS68A"
+ },
+ "execution_count": 15,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def SGD(params, gradients, lr=1e-3): \n",
+ "\n",
+ " for weights, gradient in zip(params, gradients):\n",
+ " #print(type(lr), type(gradient))\n",
+ " #print(lr, gradient)\n",
+ " weights -= lr * gradient\n",
+ " params = weights"
+ ],
+ "metadata": {
+ "id": "cKAE1bm5xPRD"
+ },
+ "execution_count": 16,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def loader(X, Y, batch_size): \n",
+ " n = X.shape[0]\n",
+ "\n",
+ " # в начале каждой эпохи будем всё перемешивать\n",
+ " # важно, что мы пермешиваем индексы, а не X\n",
+ " indices = np.arange(n)\n",
+ " np.random.shuffle(indices)\n",
+ " \n",
+ " for start in range(0, n, batch_size):\n",
+ " # в конце нам, возможно, нужно взять неполный батч\n",
+ " end = min(start + batch_size, n)\n",
+ " \n",
+ " batch_idx = indices[start:end]\n",
+ " \n",
+ " yield X[batch_idx], Y[batch_idx]"
+ ],
+ "metadata": {
+ "id": "s5aTjzA9xTgA"
+ },
+ "execution_count": 17,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "\n",
+ "n = 1000\n",
+ "\n",
+ "X = np.random.randn(n, 10)\n",
+ "true_w = np.random.randn(10, 1)\n",
+ "Y = np.dot(X, true_w).reshape(n,1)# + np.random.randn()/5\n",
+ "print('best_possible_mse:', np.mean(np.power(Y-np.dot(X, true_w).reshape(n), 2)))\n"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "i-s8Z9pPxVBw",
+ "outputId": "86d35849-7d62-4922-bafd-fa515fc4bcd2"
+ },
+ "execution_count": 18,
+ "outputs": [
+ {
+ "output_type": "stream",
+ "name": "stdout",
+ "text": [
+ "best_possible_mse: 14.871417566238062\n"
+ ]
+ }
+ ]
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "model = Sequential(\n",
+ " Linear(2, 2),\n",
+ " SoftMax()\n",
+ ")\n",
+ "\n",
+ "criterion = CrossEntropy()"
+ ],
+ "metadata": {
+ "id": "6ebfMiX6xWr5"
+ },
+ "execution_count": 19,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "n = 500\n",
+ "\n",
+ "X1 = np.random.randn(n, 2) + np.array([2, 2])\n",
+ "X2 = np.random.randn(n, 2) + np.array([-2, -2])\n",
+ "X = np.vstack([X1, X2])\n",
+ "\n",
+ "Y = np.concatenate([np.ones(n), np.zeros(n)]).astype('int')\n",
+ "Y = Y.reshape(-1,1)\n",
+ "#Y = np.hstack([Y, 1-Y])\n",
+ "#print(Y.reshape(-1,1))\n",
+ "plt.scatter(X[:,0], X[:,1], c=Y)\n",
+ "plt.show()"
+ ],
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 265
+ },
+ "id": "mXDV2aWuzgBE",
+ "outputId": "3c75cd75-8b10-4a33-a43c-bf6d8bce0321"
+ },
+ "execution_count": 20,
+ "outputs": [
+ {
+ "output_type": "display_data",
+ "data": {
+ "text/plain": [
+ "