{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "name": "practice_convnet_mnist.ipynb", "version": "0.3.2", "provenance": [], "collapsed_sections": [] }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" } }, "cells": [ { "metadata": { "id": "hffG0AKQQF7a", "colab_type": "text" }, "cell_type": "markdown", "source": [ "---" ] }, { "metadata": { "id": "R9hmyTpcQF7b", "colab_type": "text" }, "cell_type": "markdown", "source": [ "

Свёрточные нейронные сети: MNIST

" ] }, { "metadata": { "id": "XDUVcqvoQF7c", "colab_type": "text" }, "cell_type": "markdown", "source": [ "---" ] }, { "metadata": { "id": "FByx0kxrQF7e", "colab_type": "text" }, "cell_type": "markdown", "source": [ "В этом ноутбке мы научимся писать свои свёрточные нейросети на фреймворке PyTorch, и протестируем их работу на датасете MNIST.\n", "\n", "**ВНИМАНИЕ:** Рассматривается ***задача классификации изображений***.\n", "\n", "***Свёрточная нейросеть (Convolutional Neural Network, CNN)*** - это многослойная нейросеть, имеющая в своей архитектуре помимо *полносвязных слоёв* (а иногда их может и не быть) ещё и **свёрточные слои (Conv Layers)** и **pooling-слои (Pool Layers)**. \n", "\n", "Собственно, название такое эти сети получили потому, что в основе их работы лежит операция **свёртки**.\n", "\n", "Сразу же стоит сказать, что свёрточные нейросети **были придуманы прежде всего для задач, связанных с изображениями**, следовательно, на вход они тоже \"ожидают\" изображение.\n", "\n", "* Например, вот так выглядит неглубокая свёрточная нейросеть, имеющая такую архитектуру: \n", "`Input -> Conv 5x5 -> Pool 2x2 -> Conv 5x5 -> Pool 2x2 -> FC -> Output`\n", "\n", " \n", " \n", "Свёрточные нейросети (простые, есть и намного более продвинутые) почти всегда строятся по следующему правилу: \n", "\n", "`INPUT -> [[CONV -> RELU]*N -> POOL?]*M -> [FC -> RELU]*L -> FC` \n", "\n", "то есть: \n", "\n", "1). ***Входной слой*** (batch картинок -- тензор размера `(batch_size, H, W, C)`) \n", "\n", "2). $M$ блоков (M $\\ge$ 0) из свёрток и pooling-ов, причём именно в том порядке, как в формуле выше. Все эти $M$ блоков вместе называют ***feature extractor*** свёрточной нейросети, потому что эта часть сети отвечает непосредственно за формирование новых, более сложных признаков поверх тех, которые подаются (то есть, по аналогии с MLP, мы опять же переходим к новому признаковому пространству, однако здесь оно строится сложнее, чем в обычных многослойных сетях, поскольку используется операция свёртки) \n", "\n", "3). $L$ штук FullyConnected-слоёв (с активациями). Эту часть из $L$ FC-слоёв называют ***classificator***, поскольку эти слои отвечают непосредственно за предсказание нужно класса (сейчас рассматривается задача классификации изображений)." ] }, { "metadata": { "id": "vbk4cvQeQF7f", "colab_type": "text" }, "cell_type": "markdown", "source": [ "\n", "

Свёрточная нейросеть на PyTorch

\n", "\n", "Ешё раз напомним про основные компоненты нейросети:\n", "\n", "- непосредственно, сама **архитектура** нейросети (сюда входят типы функций активации у каждого нейрона);\n", "- начальная **инициализация** весов каждого слоя;\n", "- метод **оптимизации** нейросети (сюда ещё входит метод изменения `learning_rate`);\n", "- размер **батчей** (`batch_size`);\n", "- количетсво **эпох** обучения (`num_epochs`);\n", "- **функция потерь** (`loss`); \n", "- тип **регуляризации** нейросети (для каждого слоя можно свой); \n", "\n", "То, что связано с ***данными и задачей***: \n", "- само **качество** выборки (непротиворечивость, чистота, корректность постановки задачи); \n", "- **размер** выборки; \n", "\n", "Так как мы сейчас рассматриваем **архитектуру CNN**, то, помимо этих компонент, в свёрточной нейросети можно настроить следующие вещи: \n", "\n", "- (в каждом ConvLayer) **размер фильтров (окна свёртки)** (`kernel_size`)\n", "- (в каждом ConvLayer) **количество фильтров** (`out_channels`) \n", "- (в каждом ConvLayer) размер **шага окна свёртки (stride)** (`stride`) \n", "- (в каждом ConvLayer) **тип padding'а** (`padding`) \n", "\n", "\n", "- (в каждом PoolLayer) **размер окна pooling'a** (`kernel_size`) \n", "- (в каждом PoolLayer) **шаг окна pooling'а** (`stride`) \n", "- (в каждом PoolLayer) **тип pooling'а** (`pool_type`) \n", "- (в каждом PoolLayer) **тип padding'а** (`padding`)" ] }, { "metadata": { "id": "3B5KMEOHQF7g", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Какими их берут обычно -- будет показано в примере ниже. По крайней мере, можно начинать с этих настроек, чтобы понять, какое качество \"из коробки\" будет у простой модели." ] }, { "metadata": { "id": "cyZn60cgQF7h", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Посмотрим, как работает CNN на MNIST'е и на CIFAR'е:" ] }, { "metadata": { "id": "sT1gGrAEQF7i", "colab_type": "text" }, "cell_type": "markdown", "source": [ "" ] }, { "metadata": { "id": "6EXmBPFdQF7j", "colab_type": "text" }, "cell_type": "markdown", "source": [ "**MNIST:** это набор из 70k картинок рукописных цифр от 0 до 9, написанных людьми, 60k из которых являются тренировочной выборкой (`train` dataset)), и ещё 10k выделены для тестирования модели (`test` dataset)." ] }, { "metadata": { "id": "EkpzpMgOQF7k", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "#!pip install torch torchvision" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "1sS2-tgrQF7o", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "import torch\n", "import torchvision\n", "from torchvision import transforms\n", "\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "%matplotlib inline" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "ko3d1LAeQF7s", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Скачаем и загрузим данные в `DataLoader`'ы:" ] }, { "metadata": { "id": "9UTN3aXSQF7t", "colab_type": "text" }, "cell_type": "markdown", "source": [ "**Обратите внимание на аргумент `batch_size`:** именно он будет отвечать за размер батча, который будет подаваться при оптимизации нейросети" ] }, { "metadata": { "id": "oFobspK4QF7u", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "transform = transforms.Compose(\n", " [transforms.ToTensor()])\n", "\n", "trainset = torchvision.datasets.MNIST(root='./data', train=True, \n", " download=True, transform=transform)\n", "trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,\n", " shuffle=True, num_workers=2)\n", "\n", "testset = torchvision.datasets.MNIST(root='./data', train=False,\n", " download=True, transform=transform)\n", "testloader = torch.utils.data.DataLoader(testset, batch_size=4,\n", " shuffle=False, num_workers=2)\n", "\n", "classes = tuple(str(i) for i in range(10))" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "XXPTNV4IQF7x", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Сами данные лежат в полях `trainloader.dataset.train_data` и `testloader.dataset.test_data`:" ] }, { "metadata": { "scrolled": true, "id": "c2vneM-pQF7y", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "trainloader.dataset.train_data.shape" ], "execution_count": 0, "outputs": [] }, { "metadata": { "scrolled": true, "id": "YQrfHRggQF74", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "testloader.dataset.test_data.shape" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "ABm3Agg4QF79", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Выведем первую картинку:" ] }, { "metadata": { "scrolled": true, "id": "ELp398bGQF7-", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "trainloader.dataset.train_data[0]" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "_2qEWUEjQF8C", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Посмотрим, как она выглядит:" ] }, { "metadata": { "id": "MDm9FJamQF8D", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "# преобразовать тензор в np.array\n", "numpy_img = trainloader.dataset.train_data[0].numpy()" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "svvHg8TfQF8G", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "numpy_img.shape" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "A1p8zJr_QF8L", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "plt.imshow(numpy_img);" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "EMjZ0KFCQF8Q", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "plt.imshow(numpy_img, cmap='gray');" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "h5Asv7DLQF8U", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Отрисовка заданной цифры:" ] }, { "metadata": { "id": "cTaOqV7gQF8V", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "# случайный индекс от 0 до размера тренировочной выборки\n", "i = np.random.randint(low=0, high=60000)\n", "\n", "plt.imshow(trainloader.dataset.train_data[i].numpy(), cmap='gray');" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "YVE8Gq-4QF8f", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Как итерироваться по данным с помощью `loader'`а? Очень просто:" ] }, { "metadata": { "scrolled": true, "id": "FOW2fXPCQF8h", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "for data in trainloader:\n", " print(len(data))\n", " print('Images:',data[0].shape)\n", " print('Labels:', data[1].shape)\n", " break" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "4ylvXzj0QF8n", "colab_type": "text" }, "cell_type": "markdown", "source": [ "То есть мы имеем дело с кусочками данных размера batch_size (в данном случае = 4), причём в каждом батче есть как объекты, так и ответы на них (то есть и $X$, и $y$)." ] }, { "metadata": { "id": "kMJvN3OgQF8n", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Теперь вернёмся к тому, что в PyTorch есть две \"парадигмы\" построения нейросетей -- `Functional` и `Seuquential`. Со второй мы уже хорошенько разобрались в предыдущих ноутбуках по нейросетям, теперь мы испольузем именно `Functional` парадигму, потому что при построении свёрточных сетей это намного удобнее:" ] }, { "metadata": { "id": "ye9AjiYUQF8o", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "import torch.nn as nn\n", "import torch.nn.functional as F # Functional" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "9xhcsa46QF8r", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "# Заметьте: класс наследуется от nn.Module\n", "class SimpleConvNet(nn.Module):\n", " def __init__(self):\n", " # вызов конструктора предка\n", " super(SimpleConvNet, self).__init__()\n", " # необходмо заранее знать, сколько каналов у картинки (сейчас = 1),\n", " # которую будем подавать в сеть, больше ничего\n", " # про входящие картинки знать не нужно\n", " self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)\n", " self.pool = nn.MaxPool2d(kernel_size=2, stride=2)\n", " self.conv2 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5)\n", " self.fc1 = nn.Linear(4 * 4 * 16, 120) # !!!\n", " self.fc2 = nn.Linear(120, 84)\n", " self.fc3 = nn.Linear(84, 10)\n", "\n", " def forward(self, x):\n", " x = self.pool(F.relu(self.conv1(x)))\n", " x = self.pool(F.relu(self.conv2(x)))\n", " # print(x.shape)\n", " x = x.view(-1, 4 * 4 * 16) # !!!\n", " x = F.relu(self.fc1(x))\n", " x = F.relu(self.fc2(x))\n", " x = self.fc3(x)\n", " return x" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "TmkKdZy5QF8u", "colab_type": "text" }, "cell_type": "markdown", "source": [ "**Важное примечание:** Вы можете заметить, что в строчках с `#!!!` есть не очень понятное сходу число `4 * 4 * 16`. Это -- размерность тензора перед FC-слоями (H x W x C), тут её приходиться высчитывать вручную (в Keras, например, `.Flatten()` всё делает за Вас). Однако есть один *лайфхак* -- просто сделайте в `forward()` `print(x.shape)` (закомментированная строка). Вы увидите размер `(batch_size, C, H, W)` -- нужно перемножить все, кроме первого (batch_size), это и будет первая размерность `Linear()`, и именно в `C * H * W` нужно \"развернуть\" x перед подачей в `Linear()`. \n", "\n", "То есть нужно будет запустить цикл с обучением первый раз с `print()` и сделать после него `break`, посчитать размер, вписать его в нужные места и стереть `print()` и `break`." ] }, { "metadata": { "id": "HD7L63psQF8v", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Код обучения слоя:" ] }, { "metadata": { "id": "ccC8l5qDQF8w", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "from tqdm import tqdm_notebook" ], "execution_count": 0, "outputs": [] }, { "metadata": { "scrolled": true, "id": "I_872vYrQF81", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "# объявляем сеть\n", "net = SimpleConvNet()\n", "\n", "# выбираем функцию потерь\n", "loss_fn = torch.nn.CrossEntropyLoss()\n", "\n", "# выбираем алгоритм оптимизации и learning_rate\n", "learning_rate = 1e-4\n", "optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate)\n", "\n", "# итерируемся\n", "for epoch in tqdm_notebook(range(3)):\n", "\n", " running_loss = 0.0\n", " for i, batch in enumerate(tqdm_notebook(trainloader)):\n", " # так получаем текущий батч\n", " X_batch, y_batch = batch\n", " \n", " # обнуляем веса\n", " optimizer.zero_grad()\n", "\n", " # forward + backward + optimize\n", " y_pred = net(X_batch)\n", " loss = loss_fn(y_pred, y_batch)\n", " loss.backward()\n", " optimizer.step()\n", "\n", " # выведем текущий loss\n", " running_loss += loss.item()\n", " # выведем качество каждые 2000 батчей\n", " if i % 2000 == 1999:\n", " print('[%d, %5d] loss: %.3f' %\n", " (epoch + 1, i + 1, running_loss / 2000))\n", " running_loss = 0.0\n", "\n", "print('Обучение закончено')" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "qBMxgwOTQF84", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Протестируем на всём тестовом датасете, используя метрику accuracy_score:" ] }, { "metadata": { "id": "sSE-Q48CQF85", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "class_correct = list(0. for i in range(10))\n", "class_total = list(0. for i in range(10))\n", "\n", "with torch.no_grad():\n", " for data in testloader:\n", " images, labels = data\n", " y_pred = net(images)\n", " _, predicted = torch.max(y_pred, 1)\n", " c = (predicted == labels).squeeze()\n", " for i in range(4):\n", " label = labels[i]\n", " class_correct[label] += c[i].item()\n", " class_total[label] += 1\n", "\n", "for i in range(10):\n", " print('Accuracy of %5s : %2d %%' % (\n", " classes[i], 100 * class_correct[i] / class_total[i]))" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "D8ARh65-QF8-", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Два свёрточных слоя победили многослойную нейросеть (из ноутбука с домашним заданием). Это показывает эффективность применения операции свёртки при работе с изображениями." ] }, { "metadata": { "id": "bIfITzACQF8_", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Протестируем эту нейросеть на отдельных картинках из тестового датасета: напишием функцию, которая принимает индекс картинки в тестовом датасете, отрисовывает её, потом запускает на ней модель (нейросеть) и выводит результат предсказания." ] }, { "metadata": { "id": "HTp4IHRmQF9A", "colab_type": "code", "colab": {} }, "cell_type": "code", "source": [ "i = np.random.randint(low=0, high=10000)\n", "\n", "def visualize_result(index):\n", " image = testloader.dataset.test_data[index].numpy()\n", " plt.imshow(image, cmap='gray')\n", " \n", " y_pred = net(torch.Tensor(image).view(1, 1, 28, 28))\n", " _, predicted = torch.max(y_pred, 1)\n", " \n", " plt.title(f'Predicted: {predicted}')\n", "\n", "visualize_result(i)" ], "execution_count": 0, "outputs": [] }, { "metadata": { "id": "EsEFnyX_QF9D", "colab_type": "text" }, "cell_type": "markdown", "source": [ "Можете запускать ячейку выше много раз (нажимая Ctrl+Enter) и видеть, что предсказывает нейросеть в зависимости от поданной на вход картинки." ] }, { "metadata": { "id": "1VXu4ECfQF9F", "colab_type": "text" }, "cell_type": "markdown", "source": [ "

Полезные ссылки

" ] }, { "metadata": { "id": "Yz_UGrkSQF9G", "colab_type": "text" }, "cell_type": "markdown", "source": [ "1). *Примеры написания нейросетей на PyTorch (офийиальные туториалы) (на английском): https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#examples \n", "https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html*" ] }, { "metadata": { "id": "UUtXfxhlQF9G", "colab_type": "text" }, "cell_type": "markdown", "source": [ "2). Курс Стэнфорда: http://cs231n.github.io/" ] }, { "metadata": { "id": "6AsmWcvOQF9H", "colab_type": "text" }, "cell_type": "markdown", "source": [ "3). Практически исчерпывающая информация по основам свёрточных нейросетей (из cs231n) (на английском): \n", "\n", "http://cs231n.github.io/convolutional-networks/ \n", "http://cs231n.github.io/understanding-cnn/ \n", "http://cs231n.github.io/transfer-learning/" ] }, { "metadata": { "id": "gJm1GYnHQF9J", "colab_type": "text" }, "cell_type": "markdown", "source": [ "4). Видео о Computer Vision от Andrej Karpathy: https://www.youtube.com/watch?v=u6aEYuemt0M" ] } ] }