Архитектуры CNN

Базовые компоненты свёрточных сетей¶

Мы рассмотрели базовые компоненты, из которых состоят современные свёрточные нейронные сети, а также техники их обучения.

На этом занятии рассмотрим, какие модели можно построить на основе этих компонентов.

ImageNet¶

Large Scale Visual Recognition Challenge

ImageNet — один из самых известных проектов в области распознавания изображений и машинного зрения. Содержит более 14 миллионов вручную размеченных изображений, принадлежащих к 1000 классам.

Для загрузки с официального сайта необходимо запрашивать доступ, но можно загрузить данные с Kaggle. Однако архив занимает порядка 156Gb и не поместится на диск Colab. Поэтому воспользуемся другим репозиторием, в котором находится 1000 изображений из оригинального датасета.

P.S. Для загрузки данных, которые стали недоступны на официальных сайтах, можно использовать Academic Torrents. В частности для ImageNet.

In [1]:
import torch
import random
import numpy as np

# fix random_seed
torch.manual_seed(42)
random.seed(42)
np.random.seed(42)

# compute in cpu or gpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
In [2]:
# Full list of labels
#'https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json'
!wget -q https://edunet.kea.su/repo/EduNet-web_dependencies/datasets/imagenet_class_index.json

# https://github.com/ajschumacher/imagen.git
!wget -q https://edunet.kea.su/repo/EduNet-web_dependencies/datasets/imagen.zip
!unzip -q imagen.zip

Загрузили категории:

In [3]:
import json
import pprint
import numpy as np

pp = pprint.PrettyPrinter(width=41, compact=True)

with open("imagenet_class_index.json") as f:
    imagenet_labels = json.load(f)

classes = np.array(list(imagenet_labels.values()))[:, 1]

pp.pprint(
    dict(list(imagenet_labels.items())[:10])
)  # Use Pretty Print to display long dict
{'0': ['n01440764', 'tench'],
 '1': ['n01443537', 'goldfish'],
 '2': ['n01484850', 'great_white_shark'],
 '3': ['n01491361', 'tiger_shark'],
 '4': ['n01494475', 'hammerhead'],
 '5': ['n01496331', 'electric_ray'],
 '6': ['n01498041', 'stingray'],
 '7': ['n01514668', 'cock'],
 '8': ['n01514859', 'hen'],
 '9': ['n01518878', 'ostrich']}
In [4]:
from glob import glob
from PIL import Image
from torch.utils.data import Dataset


class MicroImageNet(Dataset):
    def __init__(self):
        super().__init__()
        # Load labels
        self.num2id = {}
        with open("imagenet_class_index.json") as f:
            imagenet_labels = json.load(f)
        w_net = {}
        # Because not all world net image codes from imagen exists in imagenet_labels
        # we need to filter this image
        for key in imagenet_labels.keys():
            wn_id = imagenet_labels[key][0]
            w_net[wn_id] = {"num": int(key), "name": imagenet_labels[key][1]}
        self.labels = []
        self.paths = []

        # Load data
        images = glob("imagen/*.jpg")
        images.sort()
        for i, path in enumerate(images):
            name = path.split("_")[2]  # Class name
            id = path.split("_")[0][7:]  # WorldNet based ID
            if w_net.get(id, None):
                self.labels.append([w_net[id]["num"], w_net[id]["name"], id])
                self.paths.append(path)

    def __getitem__(self, idx):
        im = Image.open(self.paths[idx])
        class_num = self.labels[idx][0]
        return im, class_num

    def __len__(self):
        return len(self.paths)


microImgNet = MicroImageNet()

Посмотрим на картинки:

In [5]:
import matplotlib.pyplot as plt

plt.rcParams["figure.figsize"] = (15, 10)


def show(img, label_1, num, label_2=""):
    ax = plt.subplot(2, 3, num + 1)
    plt.imshow(img)
    plt.title(label_1)
    ax.set_xlabel(label_2)
    plt.axis("off")


for i in range(6):
    img, label = microImgNet[i * 6]
    name = microImgNet.labels[i * 6][1]
    show(img, name, i)

Метрики Top 1 and Top5¶

ImageNet: Large Scale Visual Recognition Challenge (ILSVRC)

Source: ImageNet

На каждом изображении может быть один или несколько предметов, относящихся к одному из 1000 классов. Для метрики Тop5 алгоритм выдает метки 5 классов. Если предмет, относящийся к одному из этих классов, есть на изображении, то ответ засчитывается как верный. Для Top1, соответственно, принимается только метка одного класса.

(ILSVRC)

Лучшие результаты конкурса ILSVRC по годам.

Baseline (AlexNet 2012)¶

Создатели: Alex Krizhevsky, Ilya Sutskever, Geoffrey E. Hinton University of Toronto

Как упоминалось на первой лекции, современный бум нейросетевых технологий начался в 2012 году, когда AlexNet с большим отрывом от конкурентов победила в ImageNet.

В AlexNet есть все компоненты, которые мы рассматривали ранее. Её архитектура состоит из пяти свёрточных слоёв, между которыми располагаются pooling-слои и слои нормализации, а завершают нейросеть три полносвязных слоя.

Архитектура сети AlexNet
Source: ImageNet Classification with Deep Convolutional Neural Networks (Krizhevsky et al., 2012)

Подробнее про AlexNet

Можно заметить, что нейросеть состоит из двух работающих параллельно нейросетей, которые обмениваются информацией после 2-го и 5-го сверточного слоя и в полносвязных слоях. Это было необходимо, т.к. для обучения использовались GPU GTX580 с 3 ГБ видеопамяти. Авторы архитектуры использовали две видеокарты, работающие параллельно. На вход нейронной сети подавалось трёхканальное изображение с пространственными размерами $224 \times 224$ пикселя, к которому применялось $96$ свёрток с ядром $11 \times 11 \times 3$ и сдвигом $4$. Веса, соответствующие первым $48$ свёрткам, хранились на первой видеокарте, а оставшиеся $48$ свёрток были связаны со второй видеокартой.

  • Пространственные размеры карты признаков резко сжимаются: $224\times224 \to 55\times55 \to 27\times27 \to 13\times13 \to 13\times13 \to 13\times13$.

  • При этом увеличивается количество сверток (фильтров) в каждом слое: $96 (48\times2) \to 256 (128\times2) \to 384 (192\times2) \to 256 (128\times2)$.

  • На выходе нейросети стоят два полносвязных слоя, формирующие ответ (в ImageNet 1000 классов).

AlexNet невозможно напрямую использовать для классификации CIFAR-10. Если так агрессивно уменьшать изображение размером $32\times32$ px, то в определенный момент в него просто не поместится следующий фильтр, который нужно применить. Можно сделать resize изображения с $32\times32$ до $224\times224$, но это не самый рациональный способ использования вычислительных ресурсов.

Структура некоторых (особенно старых) сетей, заточенных под ImageNet, напрямую зависит от размера изображений. В более современных сетях есть слой Adaptive Average Pooling, позволяющий решить эту проблему, но о нем мы расскажем чуть позже.

Такая архитектура показала прорывную точность: ошибка упала с 20% до 15.4%.

Архитектура сети AlexNet c параметрами слоев

Нестандартные решения, принятые разработчиками AlexNet:

  • Использование ReLU в качестве функции активации. Предшественники использовали ограниченные функции, типа сигмоиды или тангенса, которые приводили к затуханию градиента.
  • Использование авторской нормализации. Позже было показано, что их нормализация не давала значительного прироста скорости обучения и качества модели. В современных моделях чаще используется Batch-нормализация.
  • Использование различных методов предобработки входных данных, таких как повороты, увеличение, зашумление и т.д. для увеличения разнообразия входных данных. Это называется аугментация, и о ней вы узнаете в других лекциях.
  • Использование Dropout = 0.5 (при обучении отключается случайная половина нейронов).
  • Использование SGD Momentum = 0.9. Для обеспечения сходимости авторы использовали эвристику для понижения скорости обучения (learning rate)
  • Скорость обучения стартует со значения $10^{-2}$ и уменьшается в 10 раз, когда качество перестает расти.
  • L2 регуляризация с весом $5 \cdot 10^{-4}$.
  • Использование ансамбля из 7 обученных из различного начального состояния моделей. Результат работы моделей усредняется. Данное решение увеличило точность с 81.8% до 84.6%.

Сравним PyTorch реализацию с оригинальной. В чем отличия?

In [6]:
from torchvision import models

alexnet = models.alexnet(weights="AlexNet_Weights.DEFAULT")
Downloading: "https://download.pytorch.org/models/alexnet-owt-7be5be79.pth" to /root/.cache/torch/hub/checkpoints/alexnet-owt-7be5be79.pth
100%|██████████| 233M/233M [00:02<00:00, 97.6MB/s]
In [7]:
from torchsummary import summary

print("AlexNet architecture")
print(summary(alexnet, (3, 224, 224), device="cpu"))
print(alexnet)
AlexNet architecture
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1           [-1, 64, 55, 55]          23,296
              ReLU-2           [-1, 64, 55, 55]               0
         MaxPool2d-3           [-1, 64, 27, 27]               0
            Conv2d-4          [-1, 192, 27, 27]         307,392
              ReLU-5          [-1, 192, 27, 27]               0
         MaxPool2d-6          [-1, 192, 13, 13]               0
            Conv2d-7          [-1, 384, 13, 13]         663,936
              ReLU-8          [-1, 384, 13, 13]               0
            Conv2d-9          [-1, 256, 13, 13]         884,992
             ReLU-10          [-1, 256, 13, 13]               0
           Conv2d-11          [-1, 256, 13, 13]         590,080
             ReLU-12          [-1, 256, 13, 13]               0
        MaxPool2d-13            [-1, 256, 6, 6]               0
AdaptiveAvgPool2d-14            [-1, 256, 6, 6]               0
          Dropout-15                 [-1, 9216]               0
           Linear-16                 [-1, 4096]      37,752,832
             ReLU-17                 [-1, 4096]               0
          Dropout-18                 [-1, 4096]               0
           Linear-19                 [-1, 4096]      16,781,312
             ReLU-20                 [-1, 4096]               0
           Linear-21                 [-1, 1000]       4,097,000
================================================================
Total params: 61,100,840
Trainable params: 61,100,840
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.57
Forward/backward pass size (MB): 8.38
Params size (MB): 233.08
Estimated Total Size (MB): 242.03
----------------------------------------------------------------
None
AlexNet(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
    (1): ReLU(inplace=True)
    (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (4): ReLU(inplace=True)
    (5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (7): ReLU(inplace=True)
    (8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (9): ReLU(inplace=True)
    (10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace=True)
    (12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(6, 6))
  (classifier): Sequential(
    (0): Dropout(p=0.5, inplace=False)
    (1): Linear(in_features=9216, out_features=4096, bias=True)
    (2): ReLU(inplace=True)
    (3): Dropout(p=0.5, inplace=False)
    (4): Linear(in_features=4096, out_features=4096, bias=True)
    (5): ReLU(inplace=True)
    (6): Linear(in_features=4096, out_features=1000, bias=True)
  )
)

В PyTorch версии:

  • отсутствуют нормализации,
  • отсутствует разделение на две видеокарты,
  • немного изменено количество сверток на некоторых слоях,
  • добавлен слой Adaptive Average Pooling.

Количество слоев и размеры сверток те же.

Проверим, как работает:

In [8]:
import torch
import torchvision.transforms.functional as F


def img2tensor(img):
    t = F.to_tensor(img)
    t = F.normalize(t, (0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
    return t


def catId2names(nums):
    titles = []
    for num in nums:
        titles.append(imagenet_labels[str(num.item())][1])
        titles.reverse()
    return ", ".join(titles)


for i in range(6, 12):
    img, label = microImgNet[i * 6]
    tensor = img2tensor(img)
    out = alexnet(tensor.unsqueeze(0))  # Add batch dimension
    labels_num = torch.argsort(out[0])  # Ascending order
    weights = out[0][-5:]
    predicted = catId2names(labels_num[-5:])  # Top 5
    titles = []
    name = microImgNet.labels[i * 6][1]
    show(img, name, i - 6, predicted)

Тюнинг гиперпараметров (ZFnet)¶

Visualizing and Understanding Convolutional Networks (Zeiler et al., 2013)

Архитектура сети ZFNet

Архитектура сети ZFNet

Тюнингованный AlexNet

Нейросеть ZFNet, созданная учеными из Йорского университета, в 2013 году выиграла соревнования, достигнув результата 11.7%. В ней AlexNet использовалась в качестве основы, но с изменёнными параметрами и слоями.

Отличия от AlexNet небольшие:

  • Немного поменялись пространственные размеры фильтров (было 11, стало 7);
  • Увеличилось общее количество фильтров.

Количество слоев и общая структура сети, когда слои свёртки и пулинга чередуются друг с другом, а затем идут два полносвязных слоя, сохранились.

Базовый блок (VGGNet 2014)¶

Very Deep Convolutional Networks for Large-Scale Image Recognition (Simonyan et al., 2014)

Karen Simonyan and Andrew Zisserman (Visual Geometry Group — Oxford)

Сравнение архитектур сетей AlexNet, VGG16 (использует 16 слоев) и VGG19 (использует 19 слоев)

Краткое описание VGGNet

В 2014-ом году в Оксфорде была разработана модель VGGNet.

В архитектуре VGGNet появляется идея повторения стандартных блоков и увеличения глубины нейросети повторением таких блоков (stacking). На момент создания данной архитектуры идея увеличения глубины нейросети для получения лучших результатов не была очевидна. Стандартный блок VGGNet состоит из нескольких слоев свертки (от 2 до 4) и max-pooling слоя.

Существует несколько вариантов VGGNet архитектуры. Самые известные: VGG11, VGG16 и VGG19. Цифра ставится по количеству слоев с обучаемыми весами: сверточных и полносвязных слоев.

На изображении выше показаны сети AlexNet и две версии VGG16 и VGG19 с 16 и 19 обучаемыми слоями соответственно. На соревнованиях победила более глубокая VGG19, показавшая процент ошибок при классификации top-5 7.3% (у AlexNet 15.4%).

Интересные факты о VGGNet:

  • данная архитектура получила 2-е место в задаче классификации и 1-е место в задаче локализации (классификация и обводка объекта ограничивающей рамкой) на ImageNet;
  • авторы VGGNet отказались от слоев нормализации;
  • архитектуры VGG16 или VGG19 продолжают применяться для решения ряда прикладных задач.

Особенности архитектуры VGGNet:

  • все свёрточные слои имеют фильтры с рецептивным полем размера 3×3;
  • сверточные слои объединены в блоки, состоящие из некоторого количества свёрток с разным (постепенно увеличивающимся от блока к блоку) количеством фильтров;
  • между блоками располагаются слои пулинга.

Появление "стандартных" блоков внутри модели — важное нововведение. Идея базового блока внутри сети будет достаточно широко использоваться дальше.

Разберем на коде:

In [9]:
from torchvision import models

vgg = models.vgg16(
    weights=None
)  # Change on True if you want to use VGG to predict something
print(vgg)
VGG(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace=True)
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU(inplace=True)
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU(inplace=True)
    (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU(inplace=True)
    (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace=True)
    (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU(inplace=True)
    (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): ReLU(inplace=True)
    (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): ReLU(inplace=True)
    (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): ReLU(inplace=True)
    (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): ReLU(inplace=True)
    (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (25): ReLU(inplace=True)
    (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (27): ReLU(inplace=True)
    (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (29): ReLU(inplace=True)
    (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
  (classifier): Sequential(
    (0): Linear(in_features=25088, out_features=4096, bias=True)
    (1): ReLU(inplace=True)
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU(inplace=True)
    (5): Dropout(p=0.5, inplace=False)
    (6): Linear(in_features=4096, out_features=1000, bias=True)
  )
)

Чтобы понять смысл использования сверток 3x3, необходимо познакомиться с понятием рецептивного поля и научиться оценивать количество необходимых вычислительных ресурсов.

Вычислительные ресурсы¶

Какие ресурсы нужны для работы нейронной сети?

  • Память для хранения промежуточных представлений.
  • Память для обучаемых параметров.
  • Вычислительные ресурсы.
Пример свертки с фильтрами малого размера.

Для случая когда количество каналов не меняется, фильтры и изображения квадратные, stride = 1 и padding = "same" :

$ \large Weights = K^2C^2 $ $ \large Flops = K^2C^2H^2 $

Дополнительная информация¶

Рассмотрим один сверточной слой (см. картинку выше).

$\color{orange}{\text{Оранжевый тензор}}$ — карта признаков, поступившая на вход сверточного слоя. Параметры $H_{in}$, $W_{in}$, $C_{in}$ — длина, ширина и глубина (число каналов изображения или фильтров в предыдущем сверточном слое) входного тензора. Данный тензор не учитывается в затрачиваемых ресурсах, т.к. информация сохранена в предыдущем слое.

Память для хранения промежуточных представлений определяется количеством элементов на выходе слоя ($\color{blue}{\text{синий тензор}}$). Они хранятся для вычисления следующих слоев и градиентов. Необходимая память рассчитывается как:

$$\large H_{out}⋅W_{out}⋅C_{out}⋅n_{byte},$$

где $H_{out}, W_{out}, C_{out}$ — длина, ширина и глубина (число фильтров в свертке) выходной карты признаков, а $n_{byte}$ — количество байт для хранения одного элемента (4 для float32).

Память для обучаемых параметров определяется весами и смещением (bias) фильтров свертки ($\color{green}{\text{зеленые тензоры}}$). Необходимая память рассчитывается как:

$$\large (K_h⋅K_w⋅C_{in}⋅C_{out} + С_{out})⋅n_{byte},$$

где $K_h, K_w, C_{in}$ — длина, ширина и глубина одного фильтра, $C_{out}$ — число фильтров в свертке.

Для оценки необходимых вычислительных ресурсов посчитаем количество операций сложения и умножения при прямом проходе. Каждое число выходного тензора является результатом применения фильтра свертки к некоторому рецептивному полю входного тензора. Количество операций можно оценить как произведение количества элементов на выходе слоя на размер одного фильтра:

$$\large (H_{out}⋅W_{out}⋅C_{out})⋅(K_h⋅K_w⋅C_{in}).$$

Оценка памяти для хранения параметров слоя:

In [10]:
import torch.nn as nn

conv_sizes = [11, 7, 5, 3]

for conv_size in conv_sizes:
    conv_layer = nn.Conv2d(3, 64, conv_size, stride=1, padding=1)
    print("Convolution size: %ix%i" % (conv_size, conv_size))
    for tag, p in conv_layer.named_parameters():
        print("Memory reqired for %s: %.2f kb" % (tag, (np.prod(p.shape) * 4) / 1024))
Convolution size: 11x11
Memory reqired for weight: 90.75 kb
Memory reqired for bias: 0.25 kb
Convolution size: 7x7
Memory reqired for weight: 36.75 kb
Memory reqired for bias: 0.25 kb
Convolution size: 5x5
Memory reqired for weight: 18.75 kb
Memory reqired for bias: 0.25 kb
Convolution size: 3x3
Memory reqired for weight: 6.75 kb
Memory reqired for bias: 0.25 kb

Оценка количества вычислительных операций (для одной свертки):

11×11: (224 × 224 × 64) × (3 × 11 × 11) = 9 633 792 × 11 × 11 = 9 633 792 × 121 = 1 165 688 832

7×7: (224 × 224 × 64) × (3 × 7 × 7) = 9 633 792 × 7 × 7 = 9 633 792 × 49 = 472 055 808

121/49 = ~2.5 раза меньше чем 11×11

5×5: (224 × 224 × 64) × (3 × 5 × 5) = 9 633 792 × 5 × 5 = 9 633 792 × 25 = 240 844 800

49/25 = ~2 раза меньше чем 7×7

3×3: (224 × 224 × 64) × (3 × 3 × 3) = 9 633 792 × 3 × 3 = 9 633 792 × 9 = 86 704 128

25/9 = ~2.7 раза меньше чем 5×5

Фильтры 3x3¶

Рецептивное поле — участок входной карты признаков (входного тензора), который при прохождении одного или нескольких слоев нейронной сети формирует один признак на выходе (одно число выходного тензора). Рецептивное поле можно назвать “полем зрения”.

Интуитивно кажется, что чем больше “поле зрение”, тем лучше обобщающая способность свертки. Авторы VGG решили отказаться от свертки 5x5 и заменить ее двумя свертками 3x3.

Две свёртки 3×3 вместо одной 5×5.

Рассмотрим рецептивное поле свертки 5x5 ($\color{green}{\text{зеленый квадрат}}$ на A1). Применение свертки 5x5 к этому квадрату даст 1 признак на выходе ($\color{red}{\text{красный квадрат}}$ на A3). Применение к этому же рецептивному полю свертки 3x3 ($\color{orange}{\text{оранжевые квадраты}}$ на A1) даст на 3x3 признака ($\color{red}{\text{красный квадрат}}$ на A2), применение второй свертки 3x3 позволит получить 1 признак на выходе ($\color{red}{\text{красный квадрат}}$ A3).

Итого: одна свертка 5x5 имеет то же рецептивное поле, что две свертки 3x3.

При этом применение двух сверток 3x3 дает ряд преимуществ:

  • меньшее количество параметров. При применении свертки 5x5 необходимо обучать $5⋅5⋅C_{in}⋅C_{out} = 25⋅C_{in}⋅C_{out}$ весов, где $С_{in}$ — глубина входной карты признаков, $C_{out}$ — количество фильтров сверточного слоя. При применении двух сверток 3x3 обучается $2⋅(3⋅3⋅C_{in}⋅C_{out}) = 18⋅C_{in}⋅C_{out}$ весов.
  • между свертками 3x3 добавляется дополнительный слой активации, который позволяет формировать более сложные признаки.

2 маленьких фильтра работают как один большой или даже лучше!

Аналогично 3 свертки 3x3 могут заменить одну свертку 7x7.

Три свёртки 3×3 вместо одной 7×7.

Память для хранения параметров (VGG16)¶

Архитектура VGG16 с параметрами слоев

Благодаря такой экономии получилось сделать большую по тем временам сеть (16 слоев). Тем не менее, несмотря на применённые способы уменьшения вычислительной сложности и снижение числа параметров, сеть все равно получилась огромной (16-слойная версия сети VGG расходует в 25 раз больше дорогой памяти GPU, нежели AlexNet).

Сравнение параметров AlexNet и VGG16.
  • Основная часть памяти расходуется на большие свёртки в начальных слоях, где пространственные размеры (ширина и высота) велики;

  • Больше всего весов в полносвязных слоях;

  • Вычислительные ресурсы нужны в первую очередь для сверток.

VGG-16 получилась существенно больше по сравнению с и так довольно объемной AlexNet, и тем более по сравнению с современными моделями.

В значительной степени с этим связано дальнейшее направление развития моделей. В следующем году ImageNet выиграла сеть под названием GoogLeNet.

Контроль GPU¶

In [11]:
!nvidia-smi
Tue Jul 25 12:51:18 2023       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.105.17   Driver Version: 525.105.17   CUDA Version: 12.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla T4            Off  | 00000000:00:04.0 Off |                    0 |
| N/A   38C    P8     9W /  70W |      3MiB / 15360MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

Чтобы контролировать расход памяти в процессе обучения, установим библиотеку для мониторинга ресурсов GPU:

In [12]:
!pip install -q GPUtil
  Preparing metadata (setup.py) ... done
  Building wheel for GPUtil (setup.py) ... done
In [13]:
import GPUtil as GPU
import psutil
import os


def gpu_usage():
    GPUs = GPU.getGPUs()
    # XXX: only one GPU on Colab and isn’t guaranteed
    if len(GPUs) == 0:
        return False
    gpu = GPUs[0]
    process = psutil.Process(os.getpid())
    print(
        f"GPU RAM Free: {gpu.memoryFree:.0f}MB \
    | Used: {gpu.memoryUsed:.0f}MB \
    | Util {gpu.memoryUtil*100:3.0f}% \
    | Total {gpu.memoryTotal:.0f}MB"
    )

Посмотрим, сколько памяти потребуется VGG19 и какого размера batch можно использовать.

In [14]:
import torchvision
import torch

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

vgg19 = torchvision.models.vgg19(weights=None, progress=True)
vgg19.requires_grad = True
vgg19.to(device)

gpu_usage()  # Common GPU info

vgg19.train()

for batch_size in [1, 8, 16, 32, 64]:
    input_random = torch.rand(batch_size, 3, 224, 224, device=device)
    out = vgg19(input_random)
    print("Batch size", batch_size)
    gpu_usage()
GPU RAM Free: 13928MB     | Used: 1173MB     | Util   8%     | Total 15360MB
Batch size 1
GPU RAM Free: 13392MB     | Used: 1709MB     | Util  11%     | Total 15360MB
Batch size 8
GPU RAM Free: 12578MB     | Used: 2523MB     | Util  16%     | Total 15360MB
Batch size 16
GPU RAM Free: 11498MB     | Used: 3603MB     | Util  23%     | Total 15360MB
Batch size 32
GPU RAM Free: 8948MB     | Used: 6153MB     | Util  40%     | Total 15360MB
Batch size 64
GPU RAM Free: 4632MB     | Used: 10469MB     | Util  68%     | Total 15360MB

Очистка памяти:

In [15]:
torch.cuda.empty_cache()
gpu_usage()
GPU RAM Free: 4682MB     | Used: 10419MB     | Util  68%     | Total 15360MB
In [16]:
input_random = None  # del input
out = None  # del out
gpu_usage()
GPU RAM Free: 4682MB     | Used: 10419MB     | Util  68%     | Total 15360MB
In [17]:
torch.cuda.empty_cache()
gpu_usage()
GPU RAM Free: 6076MB     | Used: 9025MB     | Util  59%     | Total 15360MB
In [18]:
vgg19 = None
gpu_usage()
GPU RAM Free: 6076MB     | Used: 9025MB     | Util  59%     | Total 15360MB
In [19]:
torch.cuda.empty_cache()

Inception module (GoogLeNet 2014)¶

Going Deeper with Convolutions (Szegedy et al., 2014)

В отличие от предыдущих нейросетей, GoolLeNet разработана в коммерческой компании с целью практического применения, поэтому основной упор был сделан на эффективность.

GoogLeNet (на самом деле архитектура называется Inception, а GoogLeNet — это имя команды в соревновании ILSVRC14, названной так в честь нейронной сети Яна Лекуна LeNet 5) — ещё более глубокая архитектура с 22 слоями. Она содержит менее 7 миллионов параметров — в 9 раз меньше, чем у AlexNet, и в 20 раз меньше, чем у VGG19. При этом сеть оказалась немного более точной, чем VGG19: ошибка снизилась с 7.3% до 6.7%.

Рассмотрим, за счёт чего удалось достичь такого огромного выигрыша в ресурсах, так как многие идеи, которые впервые были использованы для GoogLeNet, активно применяются до сих пор.

Составной блок GoogLeNet называется Inception module. Архитектура GoogLeNet состоит из множества таких блоков, следующих друг за другом.

Идея Inception module состоит в том, чтобы производить параллельные вычисления сверток с различным размером перцептивного поля и Max Pooling, конкатенируя (объединяя, а не складывая) полученные результаты. Это позволяет выделять признаки разного размера и сложности.

На рисунке выше представлена наивная реализация Inception module. Данная реализация имеет очень важный недостаток: увеличение глубины карты признаков. За счет конкатенации выходов сверток и Max Pooling из 256 каналов на входе мы получаем 672 канала на выходе. Количество каналов изображения увеличилось более чем в 2.6 раза. За 9 таких блоков глубина увеличится более чем в 5000 раз!

Такое решение плохо совместимо с экономией ресурсов.

Стоит также заметить, что слой Max Pooling в данной архитектуре имеет шаг 1 и не изменяет пространственные размеры карты признаков.

Case Study: GoogLeNet

Как мы уже показали, при использовании наивной реализации Inception module количество фильтров возрастает от слоя к слою.

Чтобы этого избежать, введены так называемые «бутылочные горлышки» — слои с фильтром 1×1, уменьшающие глубину изображения. Благодаря им удалось достичь того, чтобы количество каналов на входе и на выходе либо не менялось, либо менялось только в моменты, когда это необходимо.

Интересно, что слой свертки 1x1 ставится после слоя Max Pooling. Это позволяет более эффективно преобразовывать признаки.

Дополнительная информация¶

In [20]:
import torchvision

# https://pytorch.org/vision/stable/_modules/torchvision/models/googlenet.html#googlenet
# https://hackmd.io/@bouteille/Bk-61Fo8U
googlenet = torchvision.models.googlenet(init_weights=True)
print(googlenet)
GoogLeNet(
  (conv1): BasicConv2d(
    (conv): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
    (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
  )
  (maxpool1): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=True)
  (conv2): BasicConv2d(
    (conv): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
    (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
  )
  (conv3): BasicConv2d(
    (conv): Conv2d(64, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (bn): BatchNorm2d(192, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
  )
  (maxpool2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=True)
  (inception3a): Inception(
    (branch1): BasicConv2d(
      (conv): Conv2d(192, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (branch2): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(192, 96, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(96, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(96, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch3): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(192, 16, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(16, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(32, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=True)
      (1): BasicConv2d(
        (conv): Conv2d(192, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(32, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
  )
  (inception3b): Inception(
    (branch1): BasicConv2d(
      (conv): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (branch2): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(128, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(192, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch3): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(256, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(32, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(32, 96, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(96, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=True)
      (1): BasicConv2d(
        (conv): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
  )
  (maxpool3): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=True)
  (inception4a): Inception(
    (branch1): BasicConv2d(
      (conv): Conv2d(480, 192, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(192, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (branch2): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(480, 96, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(96, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(96, 208, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(208, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch3): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(480, 16, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(16, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(16, 48, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(48, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=True)
      (1): BasicConv2d(
        (conv): Conv2d(480, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
  )
  (inception4b): Inception(
    (branch1): BasicConv2d(
      (conv): Conv2d(512, 160, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(160, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (branch2): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(512, 112, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(112, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(112, 224, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(224, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch3): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(512, 24, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(24, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(24, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=True)
      (1): BasicConv2d(
        (conv): Conv2d(512, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
  )
  (inception4c): Inception(
    (branch1): BasicConv2d(
      (conv): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (branch2): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(256, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch3): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(512, 24, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(24, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(24, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=True)
      (1): BasicConv2d(
        (conv): Conv2d(512, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
  )
  (inception4d): Inception(
    (branch1): BasicConv2d(
      (conv): Conv2d(512, 112, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(112, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (branch2): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(512, 144, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(144, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(144, 288, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(288, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch3): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(512, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(32, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=True)
      (1): BasicConv2d(
        (conv): Conv2d(512, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(64, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
  )
  (inception4e): Inception(
    (branch1): BasicConv2d(
      (conv): Conv2d(528, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(256, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (branch2): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(528, 160, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(160, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(160, 320, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(320, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch3): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(528, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(32, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(32, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=True)
      (1): BasicConv2d(
        (conv): Conv2d(528, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
  )
  (maxpool4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=True)
  (inception5a): Inception(
    (branch1): BasicConv2d(
      (conv): Conv2d(832, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(256, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (branch2): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(832, 160, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(160, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(160, 320, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(320, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch3): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(832, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(32, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(32, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=True)
      (1): BasicConv2d(
        (conv): Conv2d(832, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
  )
  (inception5b): Inception(
    (branch1): BasicConv2d(
      (conv): Conv2d(832, 384, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(384, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (branch2): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(832, 192, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(192, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(384, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch3): Sequential(
      (0): BasicConv2d(
        (conv): Conv2d(832, 48, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(48, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
      (1): BasicConv2d(
        (conv): Conv2d(48, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (branch4): Sequential(
      (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=True)
      (1): BasicConv2d(
        (conv): Conv2d(832, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
        (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
  )
  (aux1): InceptionAux(
    (conv): BasicConv2d(
      (conv): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (fc1): Linear(in_features=2048, out_features=1024, bias=True)
    (fc2): Linear(in_features=1024, out_features=1000, bias=True)
    (dropout): Dropout(p=0.7, inplace=False)
  )
  (aux2): InceptionAux(
    (conv): BasicConv2d(
      (conv): Conv2d(528, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
      (bn): BatchNorm2d(128, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
    )
    (fc1): Linear(in_features=2048, out_features=1024, bias=True)
    (fc2): Linear(in_features=1024, out_features=1000, bias=True)
    (dropout): Dropout(p=0.7, inplace=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
  (dropout): Dropout(p=0.2, inplace=False)
  (fc): Linear(in_features=1024, out_features=1000, bias=True)
)
1x1 Convolution¶
Уменьшение глубины изображения с помощью 32 фильтров 1×1.

Свёртку 1х1 можно сравнить с линейным слоем полносвязной нейронной сети. Мы берем вектор из карты признаков (столбик 1x1x64 на картинке) и домножаем на матрицу весов (одну для всех векторов, в данном случае матрица будет 64x32, т.к. она составлена из 32 фильтров с размерами 1x1x64), чтобы получить вектор на выходе (столбик 1x1x32 на картинке).

Это одновременно формирует более сложные признаки, собирая информацию с различных сверток и Max Pooling, и позволяет сократить их количество.

Количество параметров уменьшается в два с лишним раза по сравнению с наивной реализацией. Сеть получается значительно экономичнее.

Использование таких модулей и отсутствие полносвязных слоёв делают GoogLeNet очень эффективной и достаточно точной сетью. Но это далеко не все нововведения, которые появились в этой модели.

"Stem network"¶

В составе GoogLeNet есть небольшая подсеть — Stem Network. Она состоит из трёх свёрточных слоёв (первый с большим фильтом) с двумя pooling-слоями и располагается в самом начале архитектуры. Цель этой подсети — быстро и сильно уменьшить пространственные размеры (сжать изображение перед параллельной обработкой), чтобы минимизировать количество элементов в слоях.

Архитектура GoogLeNet. В первых слоях быстро уменьшаются пространственные размеры.

Global Average Pooling¶

Отдельного внимания заслуживает завершающая часть сети. В AlexNet и VGGNet мы привыкли видеть в конце сети вытягивание карты признаков в вектор и два полносвязных слоя. В GoogLeNet один из полносвязных слоев заменен на Global Average Pooling.

Network In Network, Lin et al., 2013

Этот слой просто усредняет все значения в канале. На выходе получаем вектор признаков количество элементов в котором равно количеству каналов.

Global Average Pooling

Плюсы GAP:

  • независимость от размера входа;
  • регуляризация (препятствует переобучению);
  • уменьшение числа параметров;

В PyTorch можно реализовать GAP при помощи класса nn.AdaptiveAvgPool2d. При создании экземпляра класса указываем, какую пространственную размерность мы хотим получить на выходе. При output_size = 1получим результат как на иллюстрации.

In [21]:
gap = torch.nn.AdaptiveAvgPool2d(1)
dummy_input = torch.randn(1, 3, 6, 6)
out = gap(dummy_input)
print("Raw out shape", out.shape)
out = nn.Flatten()(out)
print("Flatten out shape", out.shape)
Raw out shape torch.Size([1, 3, 1, 1])
Flatten out shape torch.Size([1, 3])
Дополнительная информация про GAP¶

Блог-пост про Global Average Pooling

На выходе последнего Inception module формируется карта признаков 7x7x1024. Если вытянуть их в линейный слой, получится более 50 тысяч признаков.

Идея слоя Global Average Pooling (GAP) в том, что все пространственные размеры, какими бы они ни были (например, 7х7, как в GoogLeNet, или 6x6, как на картинке), сворачиваются в 1x1.

Мы берем среднее значение независимо по каждому каналу полученной карты признаков.

Ранее считалось, что применение Global Average Pooling в составе архитектуры сверточной нейронной сети (CNN), то есть осуществление поканального усреднения пространственных измерений тензора, приведёт к полной потере пространственной информации о переданном сети объекте. Тем не менее, последние исследования (Global Pooling, More than Meets the Eye: Position Information is Encoded Channel-Wise in CNNs (Islam et al., 2021)) показывают, что после такого преобразования часть пространственной информации всё же сохраняется. Несмотря на то, что семантическая информация (например информация о точных границах объекта), очевидно, полностью утрачивается после Global Average Pooling, информация об абсолютном положении объекта на исходном изображении сохраняется и оказывается закодированной порядком следования компонент в оставшемся векторе усреднённых фильтров.

Применяя GAP, мы сократили количество признаков в 49 раз! Кроме того, Global Average Pooling уменьшает переобучение, т.к. мы избавляемся от влияния менее важных признаков.

In [22]:
import torch.nn as nn
from PIL import Image
import torch


def file2tensor(filename):
    img = Image.open(filename)
    t = torchvision.transforms.functional.to_tensor(img)
    t = torchvision.transforms.functional.normalize(
        t, (0.485, 0.456, 0.406), (0.229, 0.224, 0.225)
    )
    return t


class CNNfromHW(nn.Module):
    def __init__(self, conv_module=None):
        super().__init__()
        self.activation = nn.ReLU()
        self.conv1 = nn.Conv2d(3, 16, 5, padding=2)  # 16xHxW
        self.pool = nn.MaxPool2d(2, 2)  # 16 x H/2 x W/2
        self.conv2 = nn.Conv2d(16, 32, 3, padding=1)  # 32 x H/2 x W/2
        self.gap = nn.AdaptiveAvgPool2d((1, 1))  # Any spatial size -> 32x1x1
        self.fc = nn.Linear(32, 10)

    def forward(self, x):
        print("Input shape", x.shape)
        x = self.conv1(x)  # 16xHxW
        x = self.pool(x)  # 16 x H/2 x W/2
        x = self.conv2(x)  # 32 x H/2 x W/2
        x = self.activation(x)  # Any spatial size -> 32x1x1
        x = self.gap(x)
        scores = self.fc(x.flatten(1))
        print("Output shape", scores.shape)
        return scores


print("CIFAR10 like")
input_random = torch.rand(1, 3, 32, 32)
model_with_gap = CNNfromHW()
out = model_with_gap(input_random)


print("Arbitrary size")
# Different sizes work too!
aramdillo_t = file2tensor("imagen/n02454379_10511_armadillo.jpg")
out = model_with_gap(aramdillo_t.unsqueeze(0))
CIFAR10 like
Input shape torch.Size([1, 3, 32, 32])
Output shape torch.Size([1, 10])
Arbitrary size
Input shape torch.Size([1, 3, 500, 500])
Output shape torch.Size([1, 10])

Затухание градиента¶

GoogLeNet: дополнительный классификатор

Архитектура GoogLeNet. Отдельно вынесен блок — дополнительный классификатор.

Помимо основного классификатора на выходе сети добавлены два дополнительных классификатора, встроенных в промежуточные слои. Они понадобились для того, чтобы улучшить обратное распространение градиента, потому что без батч-нормализации в таких глубоких сетях градиент очень быстро затухал, и обучить сеть такого размера было серьёзной проблемой.

Обучение VGG осуществлялось непростым способом: сначала обучали 7 слоев, затем добавляли туда следующие и обучали это вручную. Без использования батч-нормализации вряд ли получится повторить результат.

Google подошел более системно, добавив дополнительные выходы, которые способствовали тому, чтобы градиент меньше затухал. Благодаря этому удалось решить серьёзную на тот момент проблему, которая ограничивала возможность обучения глубоких моделей. Статья про батч-нормализацию появилась как раз в 15-ом году, видимо, уже после выхода этой модели.

Batchnorm (революция глубины)¶

Победители ImageNet Large Scale Visual Recognition Challenge (ILSVRC)

В 2015 году вышла статья Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift. Благодаря добавлению слоя BatchNorm стало технически возможно обучать сети состоящие из десятков слоев.

Но точность с увеличением глубины не росла:

График ошибки на обучающей и тестовой выборках двух модификаций VGGNet с 20 и 56 слоями соответственно в ходе процесса обучения
Source: Deep Residual Learning for Image Recognition (He et al., 2015)

На слайде выше приведено сравнение графиков обучения 56-слойной и 20-слойной сетей Microsoft, построенных на принципах VGGNet. Как видно из графиков, у 56-слойной сети и на тренировочном, и на тестовом датасете ошибка больше, чем у 20-слойной. Казалось бы, сеть, состоящая из большего количества слоёв, должна работать как минимум не хуже, чем сеть меньшего размера. К сожалению, проблема затухания градиента в этом случае не позволяет эффективно обучить более глубокую сеть.

В 2015 году соревнования выиграла сеть ResNet, архитектура которой предлагала новый подход к решению проблемы обучения глубоких сетей. Она состояла из 152 слоёв и снизила процент ошибок до 3,57%. Это сделало её почти в два раза эффективнее GoogLeNet.

При обучении нейронной сети методом обратного распространения ошибки модуль градиента постепенно уменьшается, проходя через каждый из слоёв сети. В глубоких сетях "длина" такого пути оказывается достаточной для того, чтобы модуль градиента стал мал и процесс обучения фактически остановился. В архитектуре ResNet такая проблема решается отказом от простого последовательного соединения слоёв (stacking) в пользу создания дополнительных связей в вычислительном графе нейронной сети, через которые градиент смог бы распространяться, минуя свёрточные слои и таким образом не затухая.

Сеть архитектуры ResNet состоит из набора так называемых Residual Block-ов. В данном блоке тензор входных признаков пропускается через пару последовательно соединённых свёрточных слоёв, после чего полученный результат складывается поканально с этим же неизменённым входным тензором. Свёрточные слои в таком блоке аппроксимируют не саму функциональную зависимость между входным и выходным тензором, а разность (анг. residual) между такой искомой зависимостью и тождественным преобразованием.

Skip connection (ResNet 2015)¶

Сравнение Residual Block с "обычными" слоями

Идея Residual Block состоит в уточнении набора признаков на каждом блоке. Вместо того, чтобы перезаписывать признаки, мы добавляем к выходу предыдущего блока уточнение, сформированное на этом блоке.

Бонусы Residual Block:

  • канал суммирования позволяет градиенту легко распространяться без затухания;
  • не теряется информация о важных признаках, выделенных на предыдущем слое.

Архитектура ResNet¶

Архитектура ResNet. Используется стаккинг residual blocks, каждый residual block состоит из двух 3x3 conv слоев, переодически количество фильтров в свертках удваивается, а размер выходных слоев уменьшается в 2 раза (за счет stride = 2). Добавился дополнительный conv слой в начале сети. Отсутствуют FC слои (кроме последнего, который предсказывает класс).

Из таких блоков можно построить очень глубокую сеть (были эксперименты из 1000 слоев). Для решения конкретной задачи — победы на ImageNet — хватило 150 слоев (добавление большего количества блоков уже не давало прироста точности).

В ResNet используются многие идеи, которые присутствовали в предыдущих моделях:

  • вначале изображение резко уменьшается,
  • дальше используются блоки 3х3, как в VGGNet,
  • применяется Average Pooling вместо полносвязного слоя.

Блоки состоят из конструкций, изображенных выше:

  • две свёртки 3Х3,
  • batch normalization после каждой свертки,
  • функции активации ReLU,
  • прибавление результата предыдущего слоя.

Stage ratio¶

Stage ratio — частота, с которой уменьшаются пространственные размеров карты признаков. Обычно записывается как последовательность чисел, например, [2,2,2,2].

Эта запись означает, что в модели 8 базовых блоков и downsampling происходит на каждом втором, что соответствует 18-слойной модели из статьи:

Source: Deep Residual Learning for Image Recognition (He et al., 2015)

Соответственно для ResNet-34 stage ratio будет $[3,4,6,3]$, для ResNet-50 — $[3,4,6,3]$, а для ResNet-101 — $[3,4,23,3]$.

BasicBlock в PyTorch¶

Код базового блока

In [23]:
import inspect
import torchvision.models.resnet as resnet

# BasicBlock
code = inspect.getsource(resnet.BasicBlock.forward)
print(code)
    def forward(self, x: Tensor) -> Tensor:
        identity = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity
        out = self.relu(out)

        return out

Так выглядит в коде базовый блок для сетей, в которых меньше 50 слоёв: свёртка, батч-нормализация, активация, свёртка, батч-нормализация.

Если свойство downsample != None, то вызывается слой downsample. Это свёртка 1x1 с шагом 2, уменьшающая пространственные размеры карты признаков.

Модель ResNet на Torchvision

Вот как инициализируются блоки (в первом сжатие происходит, во втором — нет).

In [24]:
from torchvision import models

resnet = models.resnet18(weights=None)
print(resnet.layer2)
Sequential(
  (0): BasicBlock(
    (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
    (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (relu): ReLU(inplace=True)
    (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (downsample): Sequential(
      (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
      (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (1): BasicBlock(
    (conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (relu): ReLU(inplace=True)
    (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  )
)

Bottleneck layer¶

В более глубоких сетях (ResNet-50+) для повышения эффективности используется слой "bottleneck" (похожим образом, что и в GoogLeNet)

Стоит отметить, что сеть ResNet-152 имеет существенно меньше обучаемых параметров, чем VGG-19 (58 миллионов против 144 миллионов).

Это достигнуто за счёт того, что в более глубоких сетях, помимо упомянутых Residual Block-ов с двумя свёрточными слоями с ядром 3х3, применялся более эффективный блок — bottleneck, состоящий из:

  • свёртки 1х1 с уменьшением количества фильтров,
  • свертки 3x3 с маленьким количеством фильтров,
  • свертки 1x1, восстанавливающие количество фильтров до начальных значений, чтобы к ним можно было прибавить вход.

Обучение ResNet¶

График изменения ошибки при обучении двух модификаций ResNet с 34 и 18 слоями соответственно
Source: Deep Residual Learning for Image Recognition (He et al., 2015)

При обучении ResNet шаг обучения понижали вручную, когда точность выходила на плато.

Помимо того, что ResNet c огромным отрывом выиграла ImageNet у моделей прошлого года, она стала первой моделью, превысившей точность человеческой разметки. Решения на базе этой архитектуры также стали победителями на соревнованиях по детектированию и сегментации.

Обогащение карт признаков¶

Можно взглянуть на архитектуру ResNet под таким углом: на первых слоях мы получаем базовую карту признаков, а на последующих улучшаем ее, прибавляя к ней новые уточненные признаки.

Grouped Convolution (ResNeXt 2016)¶

В AlexNet выходы первого слоя обрабатывались параллельно: половина каналов отправлялось на одну видеокарту а половина — на другую.

Только на последних слоях полученные признаки объединялись.

Можно рассматривать такой подход как ансамблирование. Но как минимум он позволяет выполнять часть операций параллельно и таким образом ускорить вычисления.

Эта идея реализуется при помощи Grouped convolution. Можно поканально разделить входной тензор признаков на произвольное количество частей (групп) и сворачивать их независимо.

Grouped Convolution с двумя группами сверток. Групповые свертки позволяют обрабатывать входные данные параллельно. В качестве бонуса — уменьшение числа параметров, и дополнительная регуляризация.

Блог-пост про Grouped Convolution

На картинке выше входная карта признаков разбивается на две группы по каналам. Над каждой группой свертки вычисляются независимо, а потом конкатенируются. Это позволяет заменить $D_{out}$ фильтров $H_{in}\times H_{in}\times D_{in}$ на $D_{out}$ фильтров $H_{in}\times H_{in}\times D_{in}/2$, сократив количество обучаемых весов в 2 раза.

Так как размер ядер станет меньше, получим выигрыш в производительности:

$$ \large Weights = \frac{C}{G} C K^2, $$

где $G$ — количество групп.

Grouped Convolution in PyTorch¶

Возможность вычисления груповой свертки в PyTorch заложена в объект nn.Conv2d. Количество групп задается параметром groups, значение которого по умолчанию равно 1.

Сравним время, которое требуется на обычную свертку с groups = 1 и groups = 64 при вычислениях на CPU:

In [25]:
# CPU test
from torch import nn
import time
import torch


def time_synchronized():
    torch.cuda.synchronize() if torch.cuda.is_available() else None
    return time.time()


input_random = torch.rand(8, 512, 112, 112)
start = time_synchronized()
normal_conv = nn.Conv2d(512, 1024, 3, groups=1)
out = normal_conv(input_random)
tm = time_synchronized() - start
print(f"Normal convolution take  {tm} sec.")

start = time_synchronized()
groupped_conv = nn.Conv2d(512, 1024, 3, groups=64)
out = groupped_conv(input_random)
tm = time_synchronized() - start
print(f"Groupped convolution take  {tm} sec.")
Normal convolution take  13.793790578842163 sec.
Groupped convolution take  0.8466033935546875 sec.

И на GPU:

In [26]:
# GPU test
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
start = time_synchronized()
normal_conv = nn.Conv2d(512, 1024, 3, groups=1).to(device)
out = normal_conv(input_random.to(device))
tm = time_synchronized() - start
print(f"Normal convolution take  {tm} sec.")

start = time_synchronized()
groupped_conv = nn.Conv2d(512, 1024, 3, groups=64).to(device)
out = groupped_conv(input_random.to(device))
tm = time_synchronized() - start
print(f"Groupped convolution take  {tm} sec.")
Normal convolution take  0.2872188091278076 sec.
Groupped convolution take  0.06319403648376465 sec.

Применение групповой свертки с groups=64 дает выигрыш во времени в 21 раз на CPU и в 41 раз на GPU. Это связано с уменьшением количества вычислительных операций.

Очистим память:

In [27]:
input_random = None
out = None

ResNeXt = Skip connection + Inception + Grouped convolution¶

2016 Aggregated Residual Transformations for Deep Neural Networks (Xie et al., 2016)

Сравнение блоков ResNet и ResNeXt. ResNeXt создан тем же коллективом авторов, что и ResNet. Увеличение ширины residual block достигается путем использования параллельных веток "cardinality"), которые по духу схожи с Inception module.

Следующая модель — ResNeXt. Эта сеть выиграла ImageNet в следующем году. Идея немного напоминает блок Inception в GoogLeNet: обрабатывать не сразу все каналы, а распараллелить обработку на несколько групп.

In [28]:
from torchvision import models
from torchsummary import summary

resnext = models.resnext50_32x4d(weights=None)

print(summary(resnext, (3, 224, 224), device="cpu"))
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1         [-1, 64, 112, 112]           9,408
       BatchNorm2d-2         [-1, 64, 112, 112]             128
              ReLU-3         [-1, 64, 112, 112]               0
         MaxPool2d-4           [-1, 64, 56, 56]               0
            Conv2d-5          [-1, 128, 56, 56]           8,192
       BatchNorm2d-6          [-1, 128, 56, 56]             256
              ReLU-7          [-1, 128, 56, 56]               0
            Conv2d-8          [-1, 128, 56, 56]           4,608
       BatchNorm2d-9          [-1, 128, 56, 56]             256
             ReLU-10          [-1, 128, 56, 56]               0
           Conv2d-11          [-1, 256, 56, 56]          32,768
      BatchNorm2d-12          [-1, 256, 56, 56]             512
           Conv2d-13          [-1, 256, 56, 56]          16,384
      BatchNorm2d-14          [-1, 256, 56, 56]             512
             ReLU-15          [-1, 256, 56, 56]               0
       Bottleneck-16          [-1, 256, 56, 56]               0
           Conv2d-17          [-1, 128, 56, 56]          32,768
      BatchNorm2d-18          [-1, 128, 56, 56]             256
             ReLU-19          [-1, 128, 56, 56]               0
           Conv2d-20          [-1, 128, 56, 56]           4,608
      BatchNorm2d-21          [-1, 128, 56, 56]             256
             ReLU-22          [-1, 128, 56, 56]               0
           Conv2d-23          [-1, 256, 56, 56]          32,768
      BatchNorm2d-24          [-1, 256, 56, 56]             512
             ReLU-25          [-1, 256, 56, 56]               0
       Bottleneck-26          [-1, 256, 56, 56]               0
           Conv2d-27          [-1, 128, 56, 56]          32,768
      BatchNorm2d-28          [-1, 128, 56, 56]             256
             ReLU-29          [-1, 128, 56, 56]               0
           Conv2d-30          [-1, 128, 56, 56]           4,608
      BatchNorm2d-31          [-1, 128, 56, 56]             256
             ReLU-32          [-1, 128, 56, 56]               0
           Conv2d-33          [-1, 256, 56, 56]          32,768
      BatchNorm2d-34          [-1, 256, 56, 56]             512
             ReLU-35          [-1, 256, 56, 56]               0
       Bottleneck-36          [-1, 256, 56, 56]               0
           Conv2d-37          [-1, 256, 56, 56]          65,536
      BatchNorm2d-38          [-1, 256, 56, 56]             512
             ReLU-39          [-1, 256, 56, 56]               0
           Conv2d-40          [-1, 256, 28, 28]          18,432
      BatchNorm2d-41          [-1, 256, 28, 28]             512
             ReLU-42          [-1, 256, 28, 28]               0
           Conv2d-43          [-1, 512, 28, 28]         131,072
      BatchNorm2d-44          [-1, 512, 28, 28]           1,024
           Conv2d-45          [-1, 512, 28, 28]         131,072
      BatchNorm2d-46          [-1, 512, 28, 28]           1,024
             ReLU-47          [-1, 512, 28, 28]               0
       Bottleneck-48          [-1, 512, 28, 28]               0
           Conv2d-49          [-1, 256, 28, 28]         131,072
      BatchNorm2d-50          [-1, 256, 28, 28]             512
             ReLU-51          [-1, 256, 28, 28]               0
           Conv2d-52          [-1, 256, 28, 28]          18,432
      BatchNorm2d-53          [-1, 256, 28, 28]             512
             ReLU-54          [-1, 256, 28, 28]               0
           Conv2d-55          [-1, 512, 28, 28]         131,072
      BatchNorm2d-56          [-1, 512, 28, 28]           1,024
             ReLU-57          [-1, 512, 28, 28]               0
       Bottleneck-58          [-1, 512, 28, 28]               0
           Conv2d-59          [-1, 256, 28, 28]         131,072
      BatchNorm2d-60          [-1, 256, 28, 28]             512
             ReLU-61          [-1, 256, 28, 28]               0
           Conv2d-62          [-1, 256, 28, 28]          18,432
      BatchNorm2d-63          [-1, 256, 28, 28]             512
             ReLU-64          [-1, 256, 28, 28]               0
           Conv2d-65          [-1, 512, 28, 28]         131,072
      BatchNorm2d-66          [-1, 512, 28, 28]           1,024
             ReLU-67          [-1, 512, 28, 28]               0
       Bottleneck-68          [-1, 512, 28, 28]               0
           Conv2d-69          [-1, 256, 28, 28]         131,072
      BatchNorm2d-70          [-1, 256, 28, 28]             512
             ReLU-71          [-1, 256, 28, 28]               0
           Conv2d-72          [-1, 256, 28, 28]          18,432
      BatchNorm2d-73          [-1, 256, 28, 28]             512
             ReLU-74          [-1, 256, 28, 28]               0
           Conv2d-75          [-1, 512, 28, 28]         131,072
      BatchNorm2d-76          [-1, 512, 28, 28]           1,024
             ReLU-77          [-1, 512, 28, 28]               0
       Bottleneck-78          [-1, 512, 28, 28]               0
           Conv2d-79          [-1, 512, 28, 28]         262,144
      BatchNorm2d-80          [-1, 512, 28, 28]           1,024
             ReLU-81          [-1, 512, 28, 28]               0
           Conv2d-82          [-1, 512, 14, 14]          73,728
      BatchNorm2d-83          [-1, 512, 14, 14]           1,024
             ReLU-84          [-1, 512, 14, 14]               0
           Conv2d-85         [-1, 1024, 14, 14]         524,288
      BatchNorm2d-86         [-1, 1024, 14, 14]           2,048
           Conv2d-87         [-1, 1024, 14, 14]         524,288
      BatchNorm2d-88         [-1, 1024, 14, 14]           2,048
             ReLU-89         [-1, 1024, 14, 14]               0
       Bottleneck-90         [-1, 1024, 14, 14]               0
           Conv2d-91          [-1, 512, 14, 14]         524,288
      BatchNorm2d-92          [-1, 512, 14, 14]           1,024
             ReLU-93          [-1, 512, 14, 14]               0
           Conv2d-94          [-1, 512, 14, 14]          73,728
      BatchNorm2d-95          [-1, 512, 14, 14]           1,024
             ReLU-96          [-1, 512, 14, 14]               0
           Conv2d-97         [-1, 1024, 14, 14]         524,288
      BatchNorm2d-98         [-1, 1024, 14, 14]           2,048
             ReLU-99         [-1, 1024, 14, 14]               0
      Bottleneck-100         [-1, 1024, 14, 14]               0
          Conv2d-101          [-1, 512, 14, 14]         524,288
     BatchNorm2d-102          [-1, 512, 14, 14]           1,024
            ReLU-103          [-1, 512, 14, 14]               0
          Conv2d-104          [-1, 512, 14, 14]          73,728
     BatchNorm2d-105          [-1, 512, 14, 14]           1,024
            ReLU-106          [-1, 512, 14, 14]               0
          Conv2d-107         [-1, 1024, 14, 14]         524,288
     BatchNorm2d-108         [-1, 1024, 14, 14]           2,048
            ReLU-109         [-1, 1024, 14, 14]               0
      Bottleneck-110         [-1, 1024, 14, 14]               0
          Conv2d-111          [-1, 512, 14, 14]         524,288
     BatchNorm2d-112          [-1, 512, 14, 14]           1,024
            ReLU-113          [-1, 512, 14, 14]               0
          Conv2d-114          [-1, 512, 14, 14]          73,728
     BatchNorm2d-115          [-1, 512, 14, 14]           1,024
            ReLU-116          [-1, 512, 14, 14]               0
          Conv2d-117         [-1, 1024, 14, 14]         524,288
     BatchNorm2d-118         [-1, 1024, 14, 14]           2,048
            ReLU-119         [-1, 1024, 14, 14]               0
      Bottleneck-120         [-1, 1024, 14, 14]               0
          Conv2d-121          [-1, 512, 14, 14]         524,288
     BatchNorm2d-122          [-1, 512, 14, 14]           1,024
            ReLU-123          [-1, 512, 14, 14]               0
          Conv2d-124          [-1, 512, 14, 14]          73,728
     BatchNorm2d-125          [-1, 512, 14, 14]           1,024
            ReLU-126          [-1, 512, 14, 14]               0
          Conv2d-127         [-1, 1024, 14, 14]         524,288
     BatchNorm2d-128         [-1, 1024, 14, 14]           2,048
            ReLU-129         [-1, 1024, 14, 14]               0
      Bottleneck-130         [-1, 1024, 14, 14]               0
          Conv2d-131          [-1, 512, 14, 14]         524,288
     BatchNorm2d-132          [-1, 512, 14, 14]           1,024
            ReLU-133          [-1, 512, 14, 14]               0
          Conv2d-134          [-1, 512, 14, 14]          73,728
     BatchNorm2d-135          [-1, 512, 14, 14]           1,024
            ReLU-136          [-1, 512, 14, 14]               0
          Conv2d-137         [-1, 1024, 14, 14]         524,288
     BatchNorm2d-138         [-1, 1024, 14, 14]           2,048
            ReLU-139         [-1, 1024, 14, 14]               0
      Bottleneck-140         [-1, 1024, 14, 14]               0
          Conv2d-141         [-1, 1024, 14, 14]       1,048,576
     BatchNorm2d-142         [-1, 1024, 14, 14]           2,048
            ReLU-143         [-1, 1024, 14, 14]               0
          Conv2d-144           [-1, 1024, 7, 7]         294,912
     BatchNorm2d-145           [-1, 1024, 7, 7]           2,048
            ReLU-146           [-1, 1024, 7, 7]               0
          Conv2d-147           [-1, 2048, 7, 7]       2,097,152
     BatchNorm2d-148           [-1, 2048, 7, 7]           4,096
          Conv2d-149           [-1, 2048, 7, 7]       2,097,152
     BatchNorm2d-150           [-1, 2048, 7, 7]           4,096
            ReLU-151           [-1, 2048, 7, 7]               0
      Bottleneck-152           [-1, 2048, 7, 7]               0
          Conv2d-153           [-1, 1024, 7, 7]       2,097,152
     BatchNorm2d-154           [-1, 1024, 7, 7]           2,048
            ReLU-155           [-1, 1024, 7, 7]               0
          Conv2d-156           [-1, 1024, 7, 7]         294,912
     BatchNorm2d-157           [-1, 1024, 7, 7]           2,048
            ReLU-158           [-1, 1024, 7, 7]               0
          Conv2d-159           [-1, 2048, 7, 7]       2,097,152
     BatchNorm2d-160           [-1, 2048, 7, 7]           4,096
            ReLU-161           [-1, 2048, 7, 7]               0
      Bottleneck-162           [-1, 2048, 7, 7]               0
          Conv2d-163           [-1, 1024, 7, 7]       2,097,152
     BatchNorm2d-164           [-1, 1024, 7, 7]           2,048
            ReLU-165           [-1, 1024, 7, 7]               0
          Conv2d-166           [-1, 1024, 7, 7]         294,912
     BatchNorm2d-167           [-1, 1024, 7, 7]           2,048
            ReLU-168           [-1, 1024, 7, 7]               0
          Conv2d-169           [-1, 2048, 7, 7]       2,097,152
     BatchNorm2d-170           [-1, 2048, 7, 7]           4,096
            ReLU-171           [-1, 2048, 7, 7]               0
      Bottleneck-172           [-1, 2048, 7, 7]               0
AdaptiveAvgPool2d-173           [-1, 2048, 1, 1]               0
          Linear-174                 [-1, 1000]       2,049,000
================================================================
Total params: 25,028,904
Trainable params: 25,028,904
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.57
Forward/backward pass size (MB): 361.78
Params size (MB): 95.48
Estimated Total Size (MB): 457.83
----------------------------------------------------------------
None

Review of ResNet Family: from ResNet to ResNeSt

Альтернативой для блока ResNeXt (рисунок слева) является блок Inception-ResNeXt (рисунок посередине), объединивший идеи ResNeXt блока и Inception блока. Применение таких блоков вместо ResNeXt позволяет немного улучшить результаты ResNeXt.

Обзор сети MobileNet (2017 г.)¶

MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications (Howard et al., 2017)

MobileNet — архитектура нейронной сети, специально созданная для работы на CPU мобильных устройств. Высокая скорость работы первой версии MobileNet достигалась благодаря разделимой по глубине свертке (Depthwise Separable Convolution).

Также для данной архитектуры предусматривается возможность варьировать размер входного изображения и ширину слоев.

Функцией активации у MobileNet обычно выступает ReLU6 (ограничение сверху у данной функции активации призвано облегчить дальнейшую 8 битную квантизацию нейронной сети).

В 2018 году была предложена архитектура MobileNetV2, которая существенно превосходит первую версию благодаря добавлению к архитектуре инвертированных остаточных блоков (Inverted Residual Block).

А в 2019 году была предложена уже MobileNetV3, которая была получена при помощи автоматического поиска архитектуры (Network Architecture Search) и дополнительно включала в себя модули squeeze-and-excitation и немонотонную функцию активации swish ("жесткая" версия которой, h-swish, так же призвана облегчить квантизацию).

Давайте разберемся, как все эти улучшения работают.

Depthwise separable convolution¶

Обычная свертка в CNN одновременно работает с шириной, высотой и глубиной (каналами) карты признаков. Разделимая по глубине свёртка (Depthwise separable convolution) разделяет вычисления на два этапа:

  • для каждого канала изображения используется свой отдельный сверточный фильтр глубины 1. По сути это Grouped Convolution с количеством групп, равным числу каналов.
  • после вычисляется свертка 1x1 с глубиной, равной глубине входной карты признаков.

Применение разделимой по глубине свертки позволяет уменьшить количество обучаемых параметров и вычислительных операций при небольшой потере точности.

DepthwiseSeparableConv = GroupedConv(group = C) + Conv(1x1)

$ \large weights = C K^2 + C^2\times1 $

vs

$ \large C^2K^2 $

Чем больше каналов, тем больше экономия. При $K=3:$

Conv2d: $ 9C^2 $

DepthwiseSeparableConv: $9C +C^2$

Inverted residual block¶

Инвертированный остаточный блок (Inverted residual block) напоминает классический residual block из архитектуры ResNet, однако имеет ряд существенных отличий. Дело в том, что классический residual block, из-за резкого уменьшения размерности пространства в комбинации с функцией активации ReLU, которая уничтожает всю информацию от отрицательных значений, приводит к потере большого количества информации. Поэтому вместо уменьшения количества слоев в середине, оно, наоборот, увеличивается. Увеличение вычислительной сложности компенсируется использованием разделимой по глубине свертки. А на входе и выходе из блока (где количество слоев уменьшается) отсутствует нелинейность (ReLU).

Сравнение моделей¶

Сравнение моделей по параметрам Точность vs Количество операций
Source: Benchmark Analysis of Representative Deep Neural Network Architectures (Bianco et al., 2018)

Картинка из статьи 2018 года (не самая свежая), но она позволяет визуально сравнить модели по трем параметрам:

  • размер модели (размер кружка),
  • количество необходимых для обучения вычислительных операций (ось x)
  • точность на задаче top-1 (ось y).

Здесь можно увидеть, что VGGNet — огромные по объёму модели, но по нынешним меркам они обладают средней точностью. Они требуют больших вычислительных ресурсов, поэтому сейчас их имеет смысл использовать разве что в учебных целях, а модели на базе ResNet (ResNet-50, ResNet-152) довольно хороши: в плане точности какого-то большого отрыва от них здесь не видно. Но, тем не менее, есть модели, которые работают чуть лучше. Рассмотрим их кратко, чтобы было понимание того, куда двигалась мысль в этой области.

Много skip connection (DenseNet 2016)¶

Densely Connected Convolutional Networks (Huang et al., 2016)

Архитектура DenseNet. Используется несколько "обходов" и конкатенация вместо суммы.

Еще один вариант, что можно сделать со слоями — добавить дополнительные связи в обход сверточных блоков, чтобы градиент проходил ещё лучше.

Можно также заменить сумму на конкатенацию. Чтобы не увеличить при этому глубину карт признаков, можно использовать свёртки 1x1.

На этих двух принципах построен DenseNet. С точки зрения ресурсов он чуть более требовательный, чем базовый ResNet, и немного более точный.

Ширина вместо глубины (WideResNet 2016)¶

Wide Residual Networks (Zagoruyko et al., 2016)

Сравнение базового ResBlock и Широкого ResBlock, где используются $F \times k$ фильтры вместо $F$ фильтров в каждом слое

Другой подход состоит в том, чтобы увеличивать не глубину, а ширину модели. Авторы утверждают, что ширина (количество фильтров) residuals блока — значительно более важный фактор, чем глубина нейронной сети. 50-слойный Wide ResNet показывает лучшие результаты, чем оригинальный 152-х слойный ResNet. С точки зрения вычислительных ресурсов, использование ширины вместо глубины позволяет более эффективно параллелить вычисления на GPU (parallelizable).

Squeeze-and-Excitation (SENet 2017)¶

При обычной свёртке значения всех каналов учитывались одинаково. Идея SE блока состоит в том, чтобы взвесить каналы, умножив каждый на свой коэффициент из интервала $[0 \dots 1]$.

Но эти коэффициенты должны зависеть от входных данных, тогда это поможет учитывать контекст. Очевидно, что найти шишку рядом с ёлкой более вероятно, чем рядом с компьютером.

Squeeze-and-Excitation Networks (Hu et al., 2017)

Channel Attention and Squeeze-and-Excitation Networks (SENet)

Основным элементом CNN являются фильтры, которые детектируют группы пикселей, обладающих локальной связностью (рецептивное поле). Сверточный фильтр объединяет:

  • пространственную информацию (spatial-wise information) — информацию о взаимном расположении пикселей относительно друг друга,
  • канальную информацию (channel-wise information) — информацию о взаимоотношениях различных каналов внутри одного локального рецептивного поля.

Он не отделяет эти два типа информации друг от друга. За счет чередования сверточных слоев и операций субдискретизации (pooling) CNN способны получать представления изображений (image representation), которые распознают сложные иерархические паттерны.

Архитектуры сверточных нейронных сетей, рассмотренные нами до этого, концентрировались на поиске лучшего представления изображения за счет улучшения способов поиска зависимости между признаками в пространстве (Inception module, Residual block и т.д.), не затрагивая отношения между каналами.

SENet (Squeeze-and-Excitation Networks) — архитектура нейронной сети, одержавшая победу в ILSVRS-2017. Создатели SENet предложили новую архитектуру блока, называемую Squeeze-and-Excitation (SE-блок), целью которой является поиск лучшего представления изображения за счет моделирования взаимодействия между каналами. Идея состоит в том, что не все каналы одинаково важны, поэтому мы можем выборочно выделять из них более информативные и подавлять менее информативные, создав механизм взвешивания каналов (feature recalibration). SE-блок состоит из следующих процессов:

  1. "Сжатие" (squeeze) каждого канала до единственного числового значения с использованием global pooling. Эта процедура позволяет получить некое глобальное представление результата обработки исходного изображения, сделанного каждым из сверточных фильтров, (global information embedding).

  2. "Возбуждение" (excitation) использует информацию, полученную на этапе "сжатия", для определения взаимодействий между каналами. Для этого используются два полносвязных слоя, первый из которых вводит "узкое место" (bottleneck), уменьшающее размерность в соответствии с параметром сжатия r, а второй восстанавливает размерность до исходной. В результате этой операции получается набор активаций, использующийся для взвешивания соответствующих каналов исходного изображения (adaptive recalibration).

Архитектура сети SENet. Добавлен модуль для взвешивания признаков, используется GAP + 2 FC слоя. Победитель ILSVRC`17 с использованием ResNeXt-152 в качестве базовой архитектуры.

Таким образом, SE-блок использует механизм, идейно напоминающий self-attention, для каналов, чьи отношения не ограничены локальным рецептивным полем соответствующих сверточных фильтров.

Описанный SE-блок может быть интегрирован в современные архитектуры сверточных нейронных сетей, например, входя в состав остаточного блока сети ResNet или Inception модуля, как изображено на рисунке.

Интеграция SE блока в современные архитектуры сверточных нейронных сетей
In [29]:
## Custom SE block


class SE_Block(nn.Module):
    "credits: https://github.com/moskomule/senet.pytorch/blob/master/senet/se_module.py#L4"

    def __init__(self, c, r=16):
        super().__init__()
        self.squeeze = nn.AdaptiveAvgPool2d(1)
        self.excitation = nn.Sequential(
            nn.Linear(c, c // r, bias=False),
            nn.ReLU(inplace=True),
            nn.Linear(c // r, c, bias=False),
            nn.Sigmoid(),
        )

    def forward(self, x):
        bs, c, _, _ = x.shape
        y = self.squeeze(x).view(bs, c)
        y = self.excitation(y).view(bs, c, 1, 1)
        print("Coefficients ", y.shape)
        return x * y.expand_as(x)


dummy = torch.randn(16, 256, 7, 7)

se_block = SE_Block(256)  # for 256 channels
print("Absolute sum", dummy.abs().sum().item())
se_out = se_block(dummy)
print("Sum after se_block", se_out.abs().sum().item())
Absolute sum 160095.890625
Coefficients  torch.Size([16, 256, 1, 1])
Sum after se_block 80070.8125

Теперь модель отключает каналы, отвечающие за признаки, которые не важны в текущем контексте:

Поиск хорошей архитектуры¶

Мы рассмотрели достаточно много блоков, из которых можно построить модель. Но как оптимально их использовать? Надо ли делать сеть максимально глубокой или надо стремиться выделить как можно больше признаков?

Neural Architecture Search¶

Neural Architecture Search with Reinforcement Learning (Zoph et al., 2016)

Схема автоматического поиска архитектуры нейронных сетей

Проектировать архитектуры нейронных сетей сложно — можно автоматизировать поиск архитектуры и пойти пить чай!

Для создания MobileNetV3 использовалась система автоматического поиска архитектуры. Вот некоторые ее особенности:

  • Рекуррентная нейронная сеть (контроллер) выводит архитектуры сетей.
  • У нас нет размеченных данных. Для того, чтобы понять, насколько хороша выведенная контроллером архитектура, создается и обучается дочерняя сеть с этой архитектурой. При этом максимизируется итоговая точность обученной дочерней сети (это — обучение без учителя или обучение с подкреплением, подробнее об обучении с подкреплением мы поговорим в других лекциях).
  • Т.к. дочерняя сеть долго обучается, есть смысл обучать несколько дочерних сетей параллельно.
  • После обучения партии дочерних сетей делаем градиентный шаг на сети контроллера (используя градиент политики).
  • Со временем контроллер учится выдавать хорошие архитектуры!
  • ОЧЕНЬ ДОРОГО!!! Каждый шаг градиента на контроллере требует обучения дочерних моделей!
  • Оригинальная статья обучалась на 800 GPU в течение 28 дней!
  • Последующие работы были сосредоточены на более эффективном поиске архитектур.

Обзор сети EfficientNet (2019 г.)¶

EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks (Tan et al., 2019)

Идея EfficientNet состоит в том, чтобы получить возможность подбирать оптимальную глубину (число слоев), ширину (число каналов в слое) и разрешение (длину и ширину карты признаков) для конкретной задачи. Например, если мы берём входное изображение больше, чем обычно (например, 1024x1024 вместо привычных 256x256), то сети потребуется больше слоёв для увеличения рецептивного поля и больше каналов для захвата более тонких деталей на большом изображении.

Source: EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks (Tan et al., 2019)

Масштабирование происходит с помощью составного коэффициента (compound coefficient).

Например, если у нас есть возможность использовать в $2^N$ больше вычислительных ресурсов, то мы можем просто увеличить глубину сети на $\alpha^N$, ширину — на $\beta^N$, и размер изображения — на $\gamma^N$, где $\alpha$, $\beta$ и $\gamma$ — постоянные коэффициенты, определяемые grid search на исходной немасштабированной модели.

Базовая сеть EfficientNet основана на инвертированных узких остаточных блоках MobileNet в дополнение к блокам сжатия и возбуждения (squeeze-and-excitation blocks).

In [30]:
from torchvision.models import efficientnet_b0
from torchsummary import summary

en_b0 = efficientnet_b0()
print(summary(en_b0, (3, 224, 224), device="cpu"))
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1         [-1, 32, 112, 112]             864
       BatchNorm2d-2         [-1, 32, 112, 112]              64
              SiLU-3         [-1, 32, 112, 112]               0
            Conv2d-4         [-1, 32, 112, 112]             288
       BatchNorm2d-5         [-1, 32, 112, 112]              64
              SiLU-6         [-1, 32, 112, 112]               0
 AdaptiveAvgPool2d-7             [-1, 32, 1, 1]               0
            Conv2d-8              [-1, 8, 1, 1]             264
              SiLU-9              [-1, 8, 1, 1]               0
           Conv2d-10             [-1, 32, 1, 1]             288
          Sigmoid-11             [-1, 32, 1, 1]               0
SqueezeExcitation-12         [-1, 32, 112, 112]               0
           Conv2d-13         [-1, 16, 112, 112]             512
      BatchNorm2d-14         [-1, 16, 112, 112]              32
           MBConv-15         [-1, 16, 112, 112]               0
           Conv2d-16         [-1, 96, 112, 112]           1,536
      BatchNorm2d-17         [-1, 96, 112, 112]             192
             SiLU-18         [-1, 96, 112, 112]               0
           Conv2d-19           [-1, 96, 56, 56]             864
      BatchNorm2d-20           [-1, 96, 56, 56]             192
             SiLU-21           [-1, 96, 56, 56]               0
AdaptiveAvgPool2d-22             [-1, 96, 1, 1]               0
           Conv2d-23              [-1, 4, 1, 1]             388
             SiLU-24              [-1, 4, 1, 1]               0
           Conv2d-25             [-1, 96, 1, 1]             480
          Sigmoid-26             [-1, 96, 1, 1]               0
SqueezeExcitation-27           [-1, 96, 56, 56]               0
           Conv2d-28           [-1, 24, 56, 56]           2,304
      BatchNorm2d-29           [-1, 24, 56, 56]              48
           MBConv-30           [-1, 24, 56, 56]               0
           Conv2d-31          [-1, 144, 56, 56]           3,456
      BatchNorm2d-32          [-1, 144, 56, 56]             288
             SiLU-33          [-1, 144, 56, 56]               0
           Conv2d-34          [-1, 144, 56, 56]           1,296
      BatchNorm2d-35          [-1, 144, 56, 56]             288
             SiLU-36          [-1, 144, 56, 56]               0
AdaptiveAvgPool2d-37            [-1, 144, 1, 1]               0
           Conv2d-38              [-1, 6, 1, 1]             870
             SiLU-39              [-1, 6, 1, 1]               0
           Conv2d-40            [-1, 144, 1, 1]           1,008
          Sigmoid-41            [-1, 144, 1, 1]               0
SqueezeExcitation-42          [-1, 144, 56, 56]               0
           Conv2d-43           [-1, 24, 56, 56]           3,456
      BatchNorm2d-44           [-1, 24, 56, 56]              48
  StochasticDepth-45           [-1, 24, 56, 56]               0
           MBConv-46           [-1, 24, 56, 56]               0
           Conv2d-47          [-1, 144, 56, 56]           3,456
      BatchNorm2d-48          [-1, 144, 56, 56]             288
             SiLU-49          [-1, 144, 56, 56]               0
           Conv2d-50          [-1, 144, 28, 28]           3,600
      BatchNorm2d-51          [-1, 144, 28, 28]             288
             SiLU-52          [-1, 144, 28, 28]               0
AdaptiveAvgPool2d-53            [-1, 144, 1, 1]               0
           Conv2d-54              [-1, 6, 1, 1]             870
             SiLU-55              [-1, 6, 1, 1]               0
           Conv2d-56            [-1, 144, 1, 1]           1,008
          Sigmoid-57            [-1, 144, 1, 1]               0
SqueezeExcitation-58          [-1, 144, 28, 28]               0
           Conv2d-59           [-1, 40, 28, 28]           5,760
      BatchNorm2d-60           [-1, 40, 28, 28]              80
           MBConv-61           [-1, 40, 28, 28]               0
           Conv2d-62          [-1, 240, 28, 28]           9,600
      BatchNorm2d-63          [-1, 240, 28, 28]             480
             SiLU-64          [-1, 240, 28, 28]               0
           Conv2d-65          [-1, 240, 28, 28]           6,000
      BatchNorm2d-66          [-1, 240, 28, 28]             480
             SiLU-67          [-1, 240, 28, 28]               0
AdaptiveAvgPool2d-68            [-1, 240, 1, 1]               0
           Conv2d-69             [-1, 10, 1, 1]           2,410
             SiLU-70             [-1, 10, 1, 1]               0
           Conv2d-71            [-1, 240, 1, 1]           2,640
          Sigmoid-72            [-1, 240, 1, 1]               0
SqueezeExcitation-73          [-1, 240, 28, 28]               0
           Conv2d-74           [-1, 40, 28, 28]           9,600
      BatchNorm2d-75           [-1, 40, 28, 28]              80
  StochasticDepth-76           [-1, 40, 28, 28]               0
           MBConv-77           [-1, 40, 28, 28]               0
           Conv2d-78          [-1, 240, 28, 28]           9,600
      BatchNorm2d-79          [-1, 240, 28, 28]             480
             SiLU-80          [-1, 240, 28, 28]               0
           Conv2d-81          [-1, 240, 14, 14]           2,160
      BatchNorm2d-82          [-1, 240, 14, 14]             480
             SiLU-83          [-1, 240, 14, 14]               0
AdaptiveAvgPool2d-84            [-1, 240, 1, 1]               0
           Conv2d-85             [-1, 10, 1, 1]           2,410
             SiLU-86             [-1, 10, 1, 1]               0
           Conv2d-87            [-1, 240, 1, 1]           2,640
          Sigmoid-88            [-1, 240, 1, 1]               0
SqueezeExcitation-89          [-1, 240, 14, 14]               0
           Conv2d-90           [-1, 80, 14, 14]          19,200
      BatchNorm2d-91           [-1, 80, 14, 14]             160
           MBConv-92           [-1, 80, 14, 14]               0
           Conv2d-93          [-1, 480, 14, 14]          38,400
      BatchNorm2d-94          [-1, 480, 14, 14]             960
             SiLU-95          [-1, 480, 14, 14]               0
           Conv2d-96          [-1, 480, 14, 14]           4,320
      BatchNorm2d-97          [-1, 480, 14, 14]             960
             SiLU-98          [-1, 480, 14, 14]               0
AdaptiveAvgPool2d-99            [-1, 480, 1, 1]               0
          Conv2d-100             [-1, 20, 1, 1]           9,620
            SiLU-101             [-1, 20, 1, 1]               0
          Conv2d-102            [-1, 480, 1, 1]          10,080
         Sigmoid-103            [-1, 480, 1, 1]               0
SqueezeExcitation-104          [-1, 480, 14, 14]               0
          Conv2d-105           [-1, 80, 14, 14]          38,400
     BatchNorm2d-106           [-1, 80, 14, 14]             160
 StochasticDepth-107           [-1, 80, 14, 14]               0
          MBConv-108           [-1, 80, 14, 14]               0
          Conv2d-109          [-1, 480, 14, 14]          38,400
     BatchNorm2d-110          [-1, 480, 14, 14]             960
            SiLU-111          [-1, 480, 14, 14]               0
          Conv2d-112          [-1, 480, 14, 14]           4,320
     BatchNorm2d-113          [-1, 480, 14, 14]             960
            SiLU-114          [-1, 480, 14, 14]               0
AdaptiveAvgPool2d-115            [-1, 480, 1, 1]               0
          Conv2d-116             [-1, 20, 1, 1]           9,620
            SiLU-117             [-1, 20, 1, 1]               0
          Conv2d-118            [-1, 480, 1, 1]          10,080
         Sigmoid-119            [-1, 480, 1, 1]               0
SqueezeExcitation-120          [-1, 480, 14, 14]               0
          Conv2d-121           [-1, 80, 14, 14]          38,400
     BatchNorm2d-122           [-1, 80, 14, 14]             160
 StochasticDepth-123           [-1, 80, 14, 14]               0
          MBConv-124           [-1, 80, 14, 14]               0
          Conv2d-125          [-1, 480, 14, 14]          38,400
     BatchNorm2d-126          [-1, 480, 14, 14]             960
            SiLU-127          [-1, 480, 14, 14]               0
          Conv2d-128          [-1, 480, 14, 14]          12,000
     BatchNorm2d-129          [-1, 480, 14, 14]             960
            SiLU-130          [-1, 480, 14, 14]               0
AdaptiveAvgPool2d-131            [-1, 480, 1, 1]               0
          Conv2d-132             [-1, 20, 1, 1]           9,620
            SiLU-133             [-1, 20, 1, 1]               0
          Conv2d-134            [-1, 480, 1, 1]          10,080
         Sigmoid-135            [-1, 480, 1, 1]               0
SqueezeExcitation-136          [-1, 480, 14, 14]               0
          Conv2d-137          [-1, 112, 14, 14]          53,760
     BatchNorm2d-138          [-1, 112, 14, 14]             224
          MBConv-139          [-1, 112, 14, 14]               0
          Conv2d-140          [-1, 672, 14, 14]          75,264
     BatchNorm2d-141          [-1, 672, 14, 14]           1,344
            SiLU-142          [-1, 672, 14, 14]               0
          Conv2d-143          [-1, 672, 14, 14]          16,800
     BatchNorm2d-144          [-1, 672, 14, 14]           1,344
            SiLU-145          [-1, 672, 14, 14]               0
AdaptiveAvgPool2d-146            [-1, 672, 1, 1]               0
          Conv2d-147             [-1, 28, 1, 1]          18,844
            SiLU-148             [-1, 28, 1, 1]               0
          Conv2d-149            [-1, 672, 1, 1]          19,488
         Sigmoid-150            [-1, 672, 1, 1]               0
SqueezeExcitation-151          [-1, 672, 14, 14]               0
          Conv2d-152          [-1, 112, 14, 14]          75,264
     BatchNorm2d-153          [-1, 112, 14, 14]             224
 StochasticDepth-154          [-1, 112, 14, 14]               0
          MBConv-155          [-1, 112, 14, 14]               0
          Conv2d-156          [-1, 672, 14, 14]          75,264
     BatchNorm2d-157          [-1, 672, 14, 14]           1,344
            SiLU-158          [-1, 672, 14, 14]               0
          Conv2d-159          [-1, 672, 14, 14]          16,800
     BatchNorm2d-160          [-1, 672, 14, 14]           1,344
            SiLU-161          [-1, 672, 14, 14]               0
AdaptiveAvgPool2d-162            [-1, 672, 1, 1]               0
          Conv2d-163             [-1, 28, 1, 1]          18,844
            SiLU-164             [-1, 28, 1, 1]               0
          Conv2d-165            [-1, 672, 1, 1]          19,488
         Sigmoid-166            [-1, 672, 1, 1]               0
SqueezeExcitation-167          [-1, 672, 14, 14]               0
          Conv2d-168          [-1, 112, 14, 14]          75,264
     BatchNorm2d-169          [-1, 112, 14, 14]             224
 StochasticDepth-170          [-1, 112, 14, 14]               0
          MBConv-171          [-1, 112, 14, 14]               0
          Conv2d-172          [-1, 672, 14, 14]          75,264
     BatchNorm2d-173          [-1, 672, 14, 14]           1,344
            SiLU-174          [-1, 672, 14, 14]               0
          Conv2d-175            [-1, 672, 7, 7]          16,800
     BatchNorm2d-176            [-1, 672, 7, 7]           1,344
            SiLU-177            [-1, 672, 7, 7]               0
AdaptiveAvgPool2d-178            [-1, 672, 1, 1]               0
          Conv2d-179             [-1, 28, 1, 1]          18,844
            SiLU-180             [-1, 28, 1, 1]               0
          Conv2d-181            [-1, 672, 1, 1]          19,488
         Sigmoid-182            [-1, 672, 1, 1]               0
SqueezeExcitation-183            [-1, 672, 7, 7]               0
          Conv2d-184            [-1, 192, 7, 7]         129,024
     BatchNorm2d-185            [-1, 192, 7, 7]             384
          MBConv-186            [-1, 192, 7, 7]               0
          Conv2d-187           [-1, 1152, 7, 7]         221,184
     BatchNorm2d-188           [-1, 1152, 7, 7]           2,304
            SiLU-189           [-1, 1152, 7, 7]               0
          Conv2d-190           [-1, 1152, 7, 7]          28,800
     BatchNorm2d-191           [-1, 1152, 7, 7]           2,304
            SiLU-192           [-1, 1152, 7, 7]               0
AdaptiveAvgPool2d-193           [-1, 1152, 1, 1]               0
          Conv2d-194             [-1, 48, 1, 1]          55,344
            SiLU-195             [-1, 48, 1, 1]               0
          Conv2d-196           [-1, 1152, 1, 1]          56,448
         Sigmoid-197           [-1, 1152, 1, 1]               0
SqueezeExcitation-198           [-1, 1152, 7, 7]               0
          Conv2d-199            [-1, 192, 7, 7]         221,184
     BatchNorm2d-200            [-1, 192, 7, 7]             384
 StochasticDepth-201            [-1, 192, 7, 7]               0
          MBConv-202            [-1, 192, 7, 7]               0
          Conv2d-203           [-1, 1152, 7, 7]         221,184
     BatchNorm2d-204           [-1, 1152, 7, 7]           2,304
            SiLU-205           [-1, 1152, 7, 7]               0
          Conv2d-206           [-1, 1152, 7, 7]          28,800
     BatchNorm2d-207           [-1, 1152, 7, 7]           2,304
            SiLU-208           [-1, 1152, 7, 7]               0
AdaptiveAvgPool2d-209           [-1, 1152, 1, 1]               0
          Conv2d-210             [-1, 48, 1, 1]          55,344
            SiLU-211             [-1, 48, 1, 1]               0
          Conv2d-212           [-1, 1152, 1, 1]          56,448
         Sigmoid-213           [-1, 1152, 1, 1]               0
SqueezeExcitation-214           [-1, 1152, 7, 7]               0
          Conv2d-215            [-1, 192, 7, 7]         221,184
     BatchNorm2d-216            [-1, 192, 7, 7]             384
 StochasticDepth-217            [-1, 192, 7, 7]               0
          MBConv-218            [-1, 192, 7, 7]               0
          Conv2d-219           [-1, 1152, 7, 7]         221,184
     BatchNorm2d-220           [-1, 1152, 7, 7]           2,304
            SiLU-221           [-1, 1152, 7, 7]               0
          Conv2d-222           [-1, 1152, 7, 7]          28,800
     BatchNorm2d-223           [-1, 1152, 7, 7]           2,304
            SiLU-224           [-1, 1152, 7, 7]               0
AdaptiveAvgPool2d-225           [-1, 1152, 1, 1]               0
          Conv2d-226             [-1, 48, 1, 1]          55,344
            SiLU-227             [-1, 48, 1, 1]               0
          Conv2d-228           [-1, 1152, 1, 1]          56,448
         Sigmoid-229           [-1, 1152, 1, 1]               0
SqueezeExcitation-230           [-1, 1152, 7, 7]               0
          Conv2d-231            [-1, 192, 7, 7]         221,184
     BatchNorm2d-232            [-1, 192, 7, 7]             384
 StochasticDepth-233            [-1, 192, 7, 7]               0
          MBConv-234            [-1, 192, 7, 7]               0
          Conv2d-235           [-1, 1152, 7, 7]         221,184
     BatchNorm2d-236           [-1, 1152, 7, 7]           2,304
            SiLU-237           [-1, 1152, 7, 7]               0
          Conv2d-238           [-1, 1152, 7, 7]          10,368
     BatchNorm2d-239           [-1, 1152, 7, 7]           2,304
            SiLU-240           [-1, 1152, 7, 7]               0
AdaptiveAvgPool2d-241           [-1, 1152, 1, 1]               0
          Conv2d-242             [-1, 48, 1, 1]          55,344
            SiLU-243             [-1, 48, 1, 1]               0
          Conv2d-244           [-1, 1152, 1, 1]          56,448
         Sigmoid-245           [-1, 1152, 1, 1]               0
SqueezeExcitation-246           [-1, 1152, 7, 7]               0
          Conv2d-247            [-1, 320, 7, 7]         368,640
     BatchNorm2d-248            [-1, 320, 7, 7]             640
          MBConv-249            [-1, 320, 7, 7]               0
          Conv2d-250           [-1, 1280, 7, 7]         409,600
     BatchNorm2d-251           [-1, 1280, 7, 7]           2,560
            SiLU-252           [-1, 1280, 7, 7]               0
AdaptiveAvgPool2d-253           [-1, 1280, 1, 1]               0
         Dropout-254                 [-1, 1280]               0
          Linear-255                 [-1, 1000]       1,281,000
================================================================
Total params: 5,288,548
Trainable params: 5,288,548
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.57
Forward/backward pass size (MB): 173.65
Params size (MB): 20.17
Estimated Total Size (MB): 194.40
----------------------------------------------------------------
None

Если вы не знаете, какую модель выбрать, используйте EfficientNet.

Self Attention (ViT 2020)¶

Visual Transformers: Token-based Image Representation and Processing for Computer Vision (Wu et al., 2020)

Реализация

Squeeze-and-Excitation Networks (Hu et al., 2019)

Vision Transformer — это модель для классификации изображений, которая использует архитектуру трансформера. Попробуем разобраться, как она работает.

В 2020 году стали появляться работы, где модели на базе архитектур трансформер смогли показать результаты лучше, чем у CNN моделей.

Source: An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale (Dosovitskiy et al., 2020)

BiT — это baseline модель на базе ResNet, ViT — Visual Transformer

Недостатки сверточного слоя¶

Авторы практически полностью отказались от использования сверток, заменив их слоями self-attention. Попробуем понять, почему это сработало.

Добавляя в модель свёрточный слой, мы руководствуемся резонным предположением: чем ближе пиксели на изображении, тем больше будет их взаимное влияние.

В большинстве случаев это работает:

  • На слое n (красный) активируются нейроны, которые реагируют на морду и на хвост кота.

  • В карте активаций их выходы оказываются рядом, и в слое n + 1 (синий) они попадают в одну свертку, которая активируется на объектах типа "кот".

Так случается часто, но не всегда:

На этом изображении активации нейронов, реагирующих на морду и хвост, не попадут в одну свертку на следующем слое. Это может привести к тому, что нейрон, обучившийся реагировать на кошек, не активируется.

Причиной этого является допущение (Inductive bias) о взаимном влиянии соседних пикселей.

Self-attention¶

Self-attention слой лишен этого недостатка. Он обучается оценивать взаимное влияние входов друг на друга. Но как применить его к изображениям?

В статье An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale (Dosovitskiy et al., 2020) предлагается разбивать картинки на кусочки (patches) размером $16\times16$ пикселей и подавать их на вход модели.

Проделаем это:

In [31]:
URL = "https://edunet.kea.su/repo/EduNet-web_dependencies/L09/cat.jpeg"
!wget -q $URL -O image.jpg

Преобразуем изображение в тензор, порежем на фрагменты и отобразим их, используя image_grid

In [32]:
from torchvision import utils, transforms
import matplotlib.pyplot as plt
import torch
from PIL import Image

img = Image.open("image.jpg")

transform = transforms.Compose([transforms.Resize((256, 256)), transforms.ToTensor()])

img = transform(img)
patches = []
sz = 64
for r in range(0, img.shape[1], sz):
    for c in range(0, img.shape[2], sz):
        patches.append(img[:, r : r + sz, c : c + sz])

patches = torch.stack(patches).type(torch.float)

img_grid = utils.make_grid(patches, pad_value=10, normalize=True, nrow=4)
plt.imshow(transforms.ToPILImage()(img_grid).convert("RGB"))
plt.axis("off")
plt.show()

На вход модели они поступят в виде вектора:

In [33]:
plt.figure(figsize=(18, 6))
img_grid = utils.make_grid(patches, pad_value=10, normalize=True, nrow=256 // 16)
plt.imshow(transforms.ToPILImage()(img_grid).convert("RGB"))
plt.axis("off");

Затем последовательность из фрагментов изображения передается в модель, где после ряда преобразований попадает на вход слоя self-attention:

Картинки приведены исключительно для наглядности, в действительности слой работает с векторами признаков, которые не визуализируются столь очевидно. Однако коэффициенты, с которыми складываются вектора признаков, отражают важность каждого с учетом всех остальных входов.

Сравнение со сверткой¶

При свертке каждый признак умножается на свой вес, и затем они суммируются. Важно, что вклад взвешенных признаков в сумму не зависит от контекста.

То есть ягода клубники, лежащая на столе (где рядом с ней может быть все, что угодно), даст такой же вклад в сумму, как и ягода с клубничного куста.

Слой self-attention выполняет ту же задачу, что и свертка: получает на вход вектор признаков и возвращает другой, более информативный. Но делает это более умно:

Вместо чисел здесь вектора, но принципильно это ничего не меняет, можно применить self-attention и к отдельным признакам (яркостям, пикселям), просто для это потребуется очень много ресурсов.

Каждый признак участвует в каждой сумме, а не только те, что попали в рецептивное поле фильтра. Кроме этого, суммируются они с коэффициентами $a$, которые зависят от входов и различны для каждой суммы.

Для получения этих коэффициентов и нужна большая часть слоя self-attention. На рисунке выделено красным.

Как получить веса внимания?¶

In [34]:
import torch
import torch.nn as nn


class SelfAttention(nn.Module):
    def __init__(self, input_dim):
        super().__init__()
        self.input_dim = input_dim
        self.query = nn.Linear(input_dim, input_dim)
        self.key = nn.Linear(input_dim, input_dim)
        self.value = nn.Linear(input_dim, input_dim)

    def forward(self, x):
        queries = self.query(x)
        keys = self.key(x)
        values = self.value(x)
        scores = torch.bmm(queries, keys.transpose(1, 2)) / (self.input_dim**0.5)
        attention = scores.softmax(dim=2)
        print("Scores shape", scores.shape)
        weighted = torch.bmm(attention, values)
        return weighted
In [35]:
embed_dim = 256
self_attention_layer = SelfAttention(embed_dim)
dummy_x = torch.randn(1, 4 * 4, embed_dim)  # Batch_size x Sequence_len x Embedding_size
out = self_attention_layer(dummy_x)
print(out.shape)
Scores shape torch.Size([1, 16, 16])
torch.Size([1, 16, 256])

Соображения относительно размера patch¶

Трансформеры работают с последовательностями за счёт механизма внимания (self-attention). И чтобы подать на вход изображение, требуется превратить его в последовательность.

Сделать это можно разными способами, например, составить последовательность из всех пикселей изображения. Её длина $n = H*W$ (высота на ширину).

Сложность вычисления одноголового слоя self-attention $O(n^2 d )$, где $n$ — число токенов и $d$ — размерность входа (embedding) (для любознательных расчеты тут).

То есть для квадратных изображений $(H==W)$ получим $O(H^3 d )$

  1. Такой подход будет очень вычислительно сложен.

  2. Интуитивно понятно, что кодировать каждый пиксель относительно большим embedding-ом не очень осмысленно.

Для тех, кто забыл, напомним что $O()$ — это Big O notation, которая отражает ресурсы, требуемые для вычисления. Так для $O(1)$ время вычисления будет постоянным вне зависимости от количества данных, а для $O(N)$ — расти пропорционально количеству данных.

Разберём на примере: Допустим, мы используем трансформер для предложения длиной в 4 слова — "Мама мылом мыла раму" => у нас есть 4 токена. Закодируем их в embeddings с размерностью 256. Потребуется порядка $4^2*256 = 4096$ операций.

А теперь попробуем провернуть то же самое для картинки размерами $256 \times 256$. Количество токенов:

$256^3*256 = 256^4 = 4 294 967 296 $.

Упс... Кажется, нам так никаких ресурсов не хватит — трансформеры с картинками использовать.

Посчитаем сложность для картинки размером $256\times256$, разбитой на кусочки по $16$ px, при том же размере токена (256).

$n = 16:$

$16^2*256 = 256^2 = 65536 $.

И впрямь! ~65000 раз меньше ресурсов требуется.

Как устроен Self-Attention

Self-Attention слой в PyTorch

Position embedding¶

Не теряем ли мы важной информации, разбивая изображение на фрагменты? На первый взгляд кажется, что модель сможет научиться восстанавливать порядок, в котором фрагменты шли в исходном изображении.

Всегда ли?

Рассмотрим пример изображения, где нет ярко выраженной текстуры:

На рисунке а) наковальня падает на ребенка, на рисунке б) ребенок прыгает на наковальне.

Суть принципиально отличается, но что будет, если составить из фрагментов любого изображения набор патчей:

Восстановить по нему можно будет любой из вариантов!

Так как self-attention блок никак не кодирует позицию элемента на входе, то важная информация потеряется.

Чтобы избежать таких потерь, информацию, кодирующую позицию фрагмента (patch), добавляют к входным данным self-attention слоя в явном виде.

Методы для кодирования позиции

Архитектура ViT¶

Теперь мы можем грузить наши изображения в Visual Transformer.

Self-attention блок мы разобрали, остальные блоки модели нам знакомы:

MLP (Multi layer perceptron) — Блок из одного или нескольких линейных слоев

Norm — Layer Normalization

Архитектура Visual Transformer
  1. Изображение режется на фрагменты (patch).
  2. Фрагменты (patch) подвергаются линейной проекции с помощью MLP.
  3. С полученными на выходе MLP векторами конкатенируются positional embeddings (кодирующие информацию о позиции path, как и в обычном трансформере для текста).
  4. К полученным векторам добавляют еще один 0*, который называют class embedding.

Любопытно, что для предсказания класса используется только выход. Он соответствует дополнительному class embedding. Остальные выходы (а для каждего токена в трансформере есть свой выход) отбрасываются за ненадобностью.

В финале этот специальный токен 0* прогоняют через MLP и предсказывают классы.

Попробуем провести аналогию со свертками. Если рассмотреть сверточную сеть без слоев пулинга и других способов изменения пространственных размеров карт признаков, то можно считать такую сеть механизмом постепенно улучшаюшем качество признаков.

Предсказание с помощью ViT¶

Используем пакет ViT PyTorch

In [36]:
!pip install -q pytorch_pretrained_vit
  Preparing metadata (setup.py) ... done
  Building wheel for pytorch_pretrained_vit (setup.py) ... done

В пакете доступны несколько предобученных моделей:

B_16, B_32, B_16_imagenet1k, ...

In [37]:
from pytorch_pretrained_vit import ViT
from torchvision import transforms

model = ViT("B_16_imagenet1k", pretrained=True)
model.eval()
Downloading: "https://github.com/lukemelas/PyTorch-Pretrained-ViT/releases/download/0.0.2/B_16_imagenet1k.pth" to /root/.cache/torch/hub/checkpoints/B_16_imagenet1k.pth
100%|██████████| 331M/331M [00:02<00:00, 122MB/s] 
Loaded pretrained weights.
Out[37]:
ViT(
  (patch_embedding): Conv2d(3, 768, kernel_size=(16, 16), stride=(16, 16))
  (positional_embedding): PositionalEmbedding1D()
  (transformer): Transformer(
    (blocks): ModuleList(
      (0-11): 12 x Block(
        (attn): MultiHeadedSelfAttention(
          (proj_q): Linear(in_features=768, out_features=768, bias=True)
          (proj_k): Linear(in_features=768, out_features=768, bias=True)
          (proj_v): Linear(in_features=768, out_features=768, bias=True)
          (drop): Dropout(p=0.1, inplace=False)
        )
        (proj): Linear(in_features=768, out_features=768, bias=True)
        (norm1): LayerNorm((768,), eps=1e-06, elementwise_affine=True)
        (pwff): PositionWiseFeedForward(
          (fc1): Linear(in_features=768, out_features=3072, bias=True)
          (fc2): Linear(in_features=3072, out_features=768, bias=True)
        )
        (norm2): LayerNorm((768,), eps=1e-06, elementwise_affine=True)
        (drop): Dropout(p=0.1, inplace=False)
      )
    )
  )
  (norm): LayerNorm((768,), eps=1e-06, elementwise_affine=True)
  (fc): Linear(in_features=768, out_features=1000, bias=True)
)
In [38]:
# Load image
!wget -q https://edunet.kea.su/repo/EduNet-web_dependencies/L09/capybara.jpg
In [39]:
capybara_in_pil = Image.open("capybara.jpg")
transforms = transforms.Compose(
    [
        transforms.Resize((384, 384)),
        transforms.ToTensor(),
        transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)),
    ]
)
capybara_in_tensor = transforms(capybara_in_pil)
print(capybara_in_tensor.shape)  # torch.Size([1, 3, 384, 384])

# Classify
with torch.no_grad():
    outputs = model(capybara_in_tensor.unsqueeze(0))
print(outputs.shape)  # (1, 1000)
torch.Size([3, 384, 384])
torch.Size([1, 1000])

Давайте посмотрим, что нам предсказывает ViT. Для этого подгрузим dict с переводом индексов в человеческие названия:

И, собственно, переведем индекс в название:

In [40]:
top3 = outputs[0].topk(3).indices
top3 = top3.tolist()


print("Top 3 predictions:")
for class_num in top3:
    print(class_num, classes[class_num])
display(capybara_in_pil.resize((384, 384)))
Top 3 predictions:
337 beaver
336 marmot
338 guinea_pig

Ну что ж, почти (капибар в классах ImageNet 1k, как вы могли догадаться, просто нет).

Обучение ViT¶

Объем данных и ресурсов¶

Как следует из текста статьи, ViT, обученный на ImageNet, уступал baseline CNN-модели на базе сверточной сети (ResNet). И только при увеличении датасетов больше, чем ImageNet, преимущество стало заметным.

Source: An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale (Dosovitskiy et al., 2020)

Вряд ли в вашем распоряжении окажется датасет, сравнимый с JFT-300M (300 миллионов изображений), и GPU/TPU ресурсы, необходимые для обучения с нуля (it could be trained using a standard cloud TPUv3 with 8 cores in approximately 30 days)

Поэтому для работы с пользовательскими данными используется техника дообучения ранее обученной модели на пользовательских данных (fine-tuning).

DeiT: Data-efficient Image Transformers¶

Для практических задач рекомендуем использовать эту реализацию. Авторы предлагают подход, благодаря которому становится возможным обучить модель на стандартном ImageNet (ImageNet1k) на одной рабочей станции за 3 дня.

We train them on a single computer in less than 3 days. Our reference vision transformer (86M parameters) achieves top-1 accuracy of 83.1% (single-crop evaluation) on ImageNet with no external data.

Source: Training data-efficient image transformers & distillation through attention

Разбор этого материала уже не входит в наш курс и рекомендуется к самостоятельному изучению.

Дополнительно:

Training data-efficient image transformers & distillation through attention (Touvron et al., 2021)

Статьи, предшествовавшие появлению ViT:

Non-local Neural Networks

CCNet: Criss-Cross Attention for Semantic Segmentation

Использование ViT с собственным датасетом¶

Для использования ViT с собственными данными рекомендуем не обучать собственную модель с нуля, а использовать уже предобученную.

Рассмотрим этот процесс на примере. Есть предобученный на ImageNet Visual Transformer, например: deit_tiny_patch16_224

И мы хотим использовать ее со своим датасетом, который может сильно отличаться от ImageNet.

Для примера возьмем CIFAR-10.

Загрузим модель. Как указано на github, модель зависит от библиотеки timm, которую нужно установить.

In [41]:
!pip install -q timm
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.2/2.2 MB 10.1 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 268.8/268.8 kB 11.5 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.3/1.3 MB 17.6 MB/s eta 0:00:00

Теперь загружаем модель с pytorch-hub:

In [42]:
import torch

model = torch.hub.load(
    "facebookresearch/deit:main", "deit_tiny_patch16_224", pretrained=True
)
Downloading: "https://github.com/facebookresearch/deit/zipball/main" to /root/.cache/torch/hub/main.zip
Downloading: "https://dl.fbaipublicfiles.com/deit/deit_tiny_patch16_224-a1311bcf.pth" to /root/.cache/torch/hub/checkpoints/deit_tiny_patch16_224-a1311bcf.pth
100%|██████████| 21.9M/21.9M [00:00<00:00, 29.5MB/s]

Убедимся, что модель запускается. Загрузим изображение:

In [43]:
!wget -q https://edunet.kea.su/repo/EduNet-web_dependencies/L09/capybara.jpg

И подадим его на вход трансформеру:

In [44]:
from timm.data.constants import IMAGENET_DEFAULT_MEAN, IMAGENET_DEFAULT_STD
import torchvision.transforms as T
from PIL import Image

pil = Image.open("capybara.jpg")

# create the data transform that DeiT expects
imagenet_transform = T.Compose(
    [
        T.Resize((224, 224)),
        T.ToTensor(),
        T.Normalize(IMAGENET_DEFAULT_MEAN, IMAGENET_DEFAULT_STD),
    ]
)

out = model(imagenet_transform(pil).unsqueeze(0))
print(out.shape)
pil.resize((224, 224))
torch.Size([1, 1000])
Out[44]:

Чтобы использовать модель с CIFAR-10, нужно поменять количество выходов слоя, отвечающих за классификацию. Так как в CIFAR-10 десять классов, а в ImageNet — тысяча.

Чтобы понять, как получить доступ к последнему слою, выведем структуру модели:

In [45]:
print(model)
VisionTransformer(
  (patch_embed): PatchEmbed(
    (proj): Conv2d(3, 192, kernel_size=(16, 16), stride=(16, 16))
    (norm): Identity()
  )
  (pos_drop): Dropout(p=0.0, inplace=False)
  (patch_drop): Identity()
  (norm_pre): Identity()
  (blocks): Sequential(
    (0): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
    (1): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
    (2): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
    (3): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
    (4): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
    (5): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
    (6): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
    (7): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
    (8): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
    (9): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
    (10): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
    (11): Block(
      (norm1): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (attn): Attention(
        (qkv): Linear(in_features=192, out_features=576, bias=True)
        (q_norm): Identity()
        (k_norm): Identity()
        (attn_drop): Dropout(p=0.0, inplace=False)
        (proj): Linear(in_features=192, out_features=192, bias=True)
        (proj_drop): Dropout(p=0.0, inplace=False)
      )
      (ls1): Identity()
      (drop_path1): Identity()
      (norm2): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
      (mlp): Mlp(
        (fc1): Linear(in_features=192, out_features=768, bias=True)
        (act): GELU(approximate='none')
        (drop1): Dropout(p=0.0, inplace=False)
        (norm): Identity()
        (fc2): Linear(in_features=768, out_features=192, bias=True)
        (drop2): Dropout(p=0.0, inplace=False)
      )
      (ls2): Identity()
      (drop_path2): Identity()
    )
  )
  (norm): LayerNorm((192,), eps=1e-06, elementwise_affine=True)
  (fc_norm): Identity()
  (head_drop): Dropout(p=0.0, inplace=False)
  (head): Linear(in_features=192, out_features=1000, bias=True)
)

Видим, что последний слой называется head и, судя по количеству параметров на выходе (1000), которое совпадает с количеством классов ImageNet, именно он отвечает за классификацию.

In [46]:
print(model.head)
Linear(in_features=192, out_features=1000, bias=True)

Заменим его слоем с 10-ю выходами по количеству классов в CIFAR-10.

In [47]:
model.head = torch.nn.Linear(192, 10, bias=True)

Убедимся, что модель не сломалась.

In [48]:
out = model(imagenet_transform(pil).unsqueeze(0))
print(out.shape)
torch.Size([1, 10])

Теперь загрузим CIFAR-10 и проверим, как дообучится модель

In [49]:
from torchvision.datasets import CIFAR10
from torch.utils.data import DataLoader

cifar10 = CIFAR10(root="./", train=True, download=True, transform=imagenet_transform)

# We use only part of CIFAR10 to reduce training time
trainset, _ = torch.utils.data.random_split(cifar10, [10000, 40000])
train_loader = DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)

testset = CIFAR10(root="./", train=False, download=True, transform=imagenet_transform)
test_loader = DataLoader(testset, batch_size=128, shuffle=False, num_workers=2)
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./cifar-10-python.tar.gz
100%|██████████| 170498071/170498071 [00:02<00:00, 81264865.69it/s]
Extracting ./cifar-10-python.tar.gz to ./
Files already downloaded and verified

Проведем стандартный цикл обучения.

In [50]:
from torch import nn
from tqdm.notebook import tqdm_notebook

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


def train(model, train_loader, optimizer, num_epochs=1):
    model.to(device)
    model.train()
    criterion = nn.CrossEntropyLoss()

    for epoch in range(num_epochs):
        for batch in tqdm_notebook(train_loader):
            inputs, labels = batch
            optimizer.zero_grad()
            outputs = model(inputs.to(device))
            loss = criterion(outputs, labels.to(device))
            loss.backward()
            optimizer.step()

Дообучаем (fine tune) только последний слой модели, который мы изменили.

In [51]:
import torch.optim as optim

model.to(device)
optimizer = optim.SGD(model.head.parameters(), lr=0.001, momentum=0.9)
train(model, train_loader, optimizer)
  0%|          | 0/79 [00:00<?, ?it/s]

Проверим точность, на всей тестовой подвыборке CIFAR-10.

In [52]:
@torch.inference_mode()
def accuracy(model, testloader):
    correct = 0
    total = 0
    for batch in testloader:
        images, labels = batch
        outputs = model(images.to(device))
        # the class with the highest energy is what we choose as prediction
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels.to(device)).sum().item()
    return correct / total
In [53]:
print(f"Accuracy of fine-tuned network : {accuracy(model, test_loader):.2f} ")
Accuracy of fine-tuned network : 0.78 

Дообучив последний слой на одной эпохе с использованием 20% данных, мы получили точность ~0.75

Если дообучить все слои на 2-х эпохах, можно получить точность порядка 0.95.

Это результат намного лучше чем тот, что мы получали на семинарах.

Для этого потребуется порядка 10 мин (на GPU). Сейчас мы этого делать не будем.

И одной из причин того, что обучение идет относительно медленно, является увеличение изображений размером $32\times32$ до $224\times224$.

Если бы мы использовали изображения CIFAR-10 в их родном размере, мы бы не потеряли никакой информации, но могли бы в разы ускорить обучение.

Изменение размеров входа ViT¶

На первый взгляд, ничего не мешает это сделать: self-attention слой работает с произвольным количеством входов.

Давайте посмотрим, что будет, если подать на вход модели изображение, отличное по размерам от $224\times224$.

Для этого перезагрузим модель:

In [54]:
def get_model():
    model = torch.hub.load(
        "facebookresearch/deit:main", "deit_tiny_patch16_224", pretrained=True
    )
    model.head = torch.nn.Linear(192, 10, bias=True)
    return model


model = get_model()
Using cache found in /root/.cache/torch/hub/facebookresearch_deit_main

И уберем из трансформаций Resize:

In [55]:
cifar_transform = T.Compose(
    [
        # T.Resize((224, 224)),    don't remove this line
        T.ToTensor(),
        T.Normalize(IMAGENET_DEFAULT_MEAN, IMAGENET_DEFAULT_STD),
    ]
)

# Change transformation in base dataset
cifar10.transform = cifar_transform
first_img = trainset[0][0]

model.to(torch.device("cpu"))
try:
    out = model(first_img.unsqueeze(0))
except Exception as e:
    print("Exception:", e)
Exception: Input image height (32) doesn't match model (224).

Получаем ошибку.

Ошибка возникает в объекте PatchEmbed, который превращает изображение в набор эмбеддингов.

У объекта есть свойство img_size, попробуем просто поменять его:

In [56]:
model.patch_embed.img_size = (32, 32)
try:
    out = model(first_img.unsqueeze(0))
except Exception as e:
    print("Exception:", e)
Exception: The size of tensor a (5) must match the size of tensor b (197) at non-singleton dimension 1

Получаем новую ошибку.

И возникает она в строке x = self.pos_drop(x + self.pos_embed)

x — это наши новые эмбеддинги для CIFAR-10 картинок

Откуда взялось число 5?

4 — это закодированные фрагменты (patch) для картинки 32х32, их всего 4 (16x16) + один embedding для предсказываемого класса(class embedding).

А 197 — это positional encoding — эмбеддинги, кодирующие позицию элемента. Они остались от ImageNet.

Так как в ImageNet картинки размера $224\times224$, то в каждой помещалось $14\times14 = 196$ фрагментов и еще embedding для класса, итого $197$ позиций.

Эмбеддинги для позиций доступны через свойство:

In [57]:
model.pos_embed.data.shape
Out[57]:
torch.Size([1, 197, 192])

Теперь нам надо изменить количество pos embeddings так, чтобы оно было равно 5 (количество patch + 1). Возьмем 5 первых:

In [58]:
model.pos_embed.data = model.pos_embed.data[:, :5, :]
out = model(first_img.unsqueeze(0))
print(out.shape)
torch.Size([1, 10])

Заработало!

Теперь обучим модель. Так как изображения стали намного меньше, то мы можем увеличить размер batch и использовать весь датасет. Также будем обучать все слои, а не только последний.

In [59]:
cifar10.transform = cifar_transform
train_loader = DataLoader(cifar10, batch_size=512, shuffle=True, num_workers=2)

# Now we train all parameters because model altered
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
train(model, train_loader, optimizer)
  0%|          | 0/98 [00:00<?, ?it/s]

Сильно быстрее. Посмотрим на результат:

In [60]:
testset.transform = cifar_transform
print(f"Accuracy of altered network : {accuracy(model,test_loader):.2f} ")
Accuracy of altered network : 0.53 

Сильно хуже.

Это можно объяснить тем, что маленькие patch ImageNet(1/196) семантически сильно отличаются от четвертинок картинок из CIFAR-10 (1/4).

Но есть и другая причина: мы взяли лишь первые 4 pos_embedding а остальные отбросили. В итоге модель вынуждена практически заново обучаться работать с малым pos_embedding, и двух эпох для этого мало.

Зато теперь мы можем использовать модель с изображениями любого размера.

ConvNext (2022)¶

Авторы продемонстрировали, что сверточная сеть может превзойти трансформер и так же хорошо масштабироваться.

Source: A ConvNet for the 2020s

При этом особенных нововведений не потребовалось. Была проведена ревизия процесса обучения. И изменены некоторые параметры модели. За основу был взят обычный ResNet.

Процесс обучения¶

Использовали алгоритм обучения такой же как у трансформера:

  • Оптимизатор Adam заменили на AdamW.
  • Увеличили количество эпох с 90 до 300.
  • Расширили набор аугментаций (Mixup, Cutmix, RandAugment, Random Erasing).
  • Добавили регуляризацию (Stochastic Depth).
  • Использовали Label Smoothing(предсказание не одной метки, а вероятностей для разных классов).

Ablation study¶

Source: A ConvNet for the 2020s

Macro Design

  • Свертка $7\times7$ с шагом $2$ в блоке сжатия заменена сверткой $4\times4$ с шагом $4$ как у SWIN Transformer

  • stage ration поменялся с $(3, 4, 6, 3)$ на $(3, 3, 9, 3)$.

Micro design

  • Пулинги заменены свертками $2\times2$ с шагом $2$.

ConvNeXt V2: Co-designing and Scaling ConvNets with Masked Autoencoders

Model soups (2022)¶

Source: Model soups: averaging weights of multiple fine-tuned models improves accuracy without increasing inference time

Обучается несколько моделей, а затем их веса усредняются. Работает для различных архитектур.

Обучение без разметки¶

Все пречисленные выше модели учились при помощи классического обучения с учителем: на размеченных данных из ImageNet.

Однако данных как в открытом, так и закрытом доступе с каждым годом становится все больше. Логично предположить, что из этих данных можно извлечь дополнительную информацию, которая поможет лучше классифицировать старый добрый ImageNet.

Модели и алгоритмы, о которых пойдет речь дальше, тем или иным образом используют эти внешние данные.

Дистилляция¶

Knowledge distillation — техника, которая используется для обучения одной модели (маленькой) при помощи другой (большой).

Представим, что у нас нет больше доступа к ImageNet, но на диске осталась модель, которую на нем учили, например Efficientnet_b7. Модель большая и точная, а нам нужно решить некоторую простую задачу, например, отличать котов от собак. Но делать это на мобильном устройстве и очень быстро.

Hard targets¶

Можно воспользоваться большой моделью, и с ее помощью разметить имеющиеся неразмеченные изображения, а затем на этих данных учить маленькую и быструю модель.

  1. img $\to$ BIG_NET $\to$ logit $\to$ softmax $\to$ one_hot $\to$ label.
  2. img $\to$ small_net $\to$ loss(logit, label) $\to$ backprop.

Soft targets¶

Подход, описанный выше, будет работать, но можно поступить и хитрее.

Предположим, большая сеть классифицирует MNIST, и для цифры:

мы получили вот такой вектор логитов

In [61]:
from matplotlib import pyplot as plt

logits = [0.1, 0.1, 0.4, 5, 0.1, 0.2, 0.1, 0.2, 3, 0.7]

plt.figure(figsize=(6, 3))
plt.bar(range(0, 10), logits)
plt.xticks(range(0, 10))
plt.show()

Самый большой логит у тройки, что верно. Но и у восьмерки он большой, так как эти цифры похожи.

То есть логиты содержат дополнительную полезную информацию.

Когда мы превратим их в вероятности, она сохранится.

In [62]:
import torch
from torch.nn.functional import softmax

probs = softmax(torch.tensor(logits), dim=0)

plt.figure(figsize=(6, 3))
plt.bar(range(0, 10), probs)
plt.xticks(range(0, 10))
plt.show()

А вот после того, как мы превратим вектор вероятностей в метку (one_hot вектор), эта доп. информация пропадет при округлении.

In [63]:
one_hot = (probs >= probs.max()).int()
print("One hot ", one_hot)
One hot  tensor([0, 0, 0, 1, 0, 0, 0, 0, 0, 0], dtype=torch.int32)
In [64]:
import numpy as np

f, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 4), sharey=False)


def bar(ax, y, title):
    x = range(0, 10)
    ax.bar(x, np.array(y))
    ax.set_title(title)


bar(ax1, logits, "Logits")
bar(ax2, probs, "Probs")
bar(ax3, one_hot, "Label")

plt.show()

Чтобы не терять информацию, можно предсказывать не метку, а весь вектор вероятностей, который получается на выходе большой модели .

Feature extraction¶

В процессе выполнения задачи классификации картинок мы получаем карты признаков, которые отвечают за распознавание разных особенностей и паттернов на изображениях. Эту карту признаков мы используем для решения задачи классификации.

Эти же признаки можно использовать для решения других задач. Для этого можно заменить финальные слои нейросети:

На основании карт признаков можно проводить детектирование объектов, сегментацию, генерировать вектора признаков (embedding) для разных задач: сравнения изображений, распознавания лиц, трекинга и т.д.

Часть сети (слева) является достаточно универсальной.

Можно обучить веса, обучая сеть классифицировать изображения, а потом использовать предобученный фрагмент для вычисления карты признаков для любой другой задачи.

Часть нейросети, вычисляющая карту признаков, называется backbone (скелет, основа).

BYOL¶

Source: Bootstrap Your Own Latent

Методика, описанная в статье Bootstrap Your Own Latent, использует дистилляцию для получения качественного представления данных в пространстве признаков (latent space).

Идеи метода:

  1. В интернете масса неразмеченных изображений, давайте будем всячески искажать(аугментировать) их: обрезать, масштабировать, поворачивать и затем подавать на вход сети.

Если искажения не меняют объект до неузнаваемости, то его класс поменяться не должен, следовательно, можно требовать от модели, чтобы вектора признаков у аугментированных изображений были похожи.

Так как меток у нас нет, то в качестве loss можно использовать расстояние между векторами признаков. Для векторов, полученных из одного изображения, оно должно быть меньше, чем между векторами различных изображений. Эта идея Contrastive Loss, которая подробно будет обсуждаться в 11-й лекции.

При наивной реализации такой подход работает не слишком хорошо, так как общее количество сравнений для датасета с N изображениями и M аугментациями — это $ (N*M)^2$ Поэтому приходится выбирать из всех возможных пар только часть самых сложных (Hard example Mining ).

  1. Авторы статьи предложили решение, основанное на идее дистилляции.

Создаются две одинаковые сети (online и target) и два различных набора аугментаций (t и t`). В первую сеть подаются изображения, аугментированные при помощи t, во вторую — при помощи t'.

Предсказания моделей сравниваются, и loss подсчитываться как расстояние между предсказаниями, но обновление весов методом градиентного спуска происходит только у одной модели (online).

Веса второй(target) постепенно обновляются как экспоненциальное скользящее среднее от весов первой модели.

$\xi \leftarrow \tau \xi + (1-\tau\theta),$

где $\tau$ — скорость обновления, $\theta$ — веса другой(online) модели.

Source: Bootstrap Your Own Latent

Затем достаточно получить embedding-и для изображений из ImageNet и классифицировать их при помощи линейного классификатора.

Большая библиотека, где реализован BYOL: документация и репозиторий.

Чтобы не качать зависимости, воспользуемся этим репозиторием.

In [65]:
!pip install -q byol-pytorch
In [66]:
import torch
from byol_pytorch import BYOL
from torchvision import models
from torchvision.datasets import ImageFolder, DatasetFolder
from tqdm import tqdm
from warnings import simplefilter

simplefilter("ignore", UserWarning)

resnet = models.resnet50(weights=None)

learner = BYOL(resnet, image_size=256, hidden_layer="avgpool")

learner.to(device)
opt = torch.optim.Adam(learner.parameters(), lr=3e-4)


def sample_unlabelled_images():
    return torch.randn(20, 3, 256, 256)


for _ in tqdm(range(3)):
    images = sample_unlabelled_images()
    loss = learner(images.to(device))
    opt.zero_grad()
    loss.backward()
    opt.step()
    learner.update_moving_average()  # update moving average of target encoder

# save your improved network
torch.save(resnet.state_dict(), "./improved-net.pt")
100%|██████████| 3/3 [00:02<00:00,  1.27it/s]

CLIP¶

Contrastive Language-Image Pre-training

Source: Learning Transferable Visual Models From Natural Language Supervision

Для обучения модели был создан новый датасет, включающий 400M пар изображение–текст.

Source: Learning Transferable Visual Models From Natural Language Supervision

Для использования в качестве классификатора достаточно создать текстовый эмбеддинг для слова, описывающего каждый класс, а затем сравнить его с эмбеддингом изображения.

Source: Learning Transferable Visual Models From Natural Language Supervision

Точность классификации не дотягивает до SOTA supervised моделей, но зато работает без дообучения на различных датасетах.

Как использовать¶

Установка:

In [67]:
!pip install -q git+https://github.com/openai/CLIP.git
  Preparing metadata (setup.py) ... done
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 53.1/53.1 kB 1.5 MB/s eta 0:00:00
  Building wheel for clip (setup.py) ... done
In [68]:
img, class_num = microImgNet[200]
show(img, microImgNet.labels[200][1], 0)

На вход CLIP надо подать список текстов, на выходе получим оценку того, какой из них больше всего подходит к изображению.

In [69]:
texts = [
    "A man with a gasoline saw is getting firewood",
    "Santa Claus sleigh",
    "chain saw",
    "cat",
    "dog",
]

Можно использовать разные backbone:

In [70]:
import clip

print(clip.available_models())
['RN50', 'RN101', 'RN50x4', 'RN50x16', 'RN50x64', 'ViT-B/32', 'ViT-B/16', 'ViT-L/14', 'ViT-L/14@336px']

Создаем экземпляр модели:

In [71]:
import clip

model, preprocess = clip.load("ViT-B/32", device="cpu")
100%|███████████████████████████████████████| 338M/338M [00:04<00:00, 76.2MiB/s]

Дополнительно получили объект preprocess с необходимыми трансформациями. Подготовим с их помощью изображение и токенизируем текст:

In [72]:
image = preprocess(img).unsqueeze(0)
text = clip.tokenize(texts)

Теперь можно запускать

Given a batch of images and a batch of text tokens, returns two Tensors, containing the logit scores corresponding to each image and text input. The values are cosine similarities between the corresponding image and text features, times 100.

In [73]:
with torch.no_grad():
    logits_per_image, _ = model(image, text)
    probs = logits_per_image.softmax(dim=-1).numpy()

print(probs)

plt.figure(figsize=(6, 3))
plt.bar(range(len(texts)), probs.flatten())
plt.show()
[[9.9127918e-01 5.4925272e-06 8.7150745e-03 5.6151421e-08 2.9277066e-07]]

Получение embedding¶

Вот так можно извлечь признаки для картинки и для текста:

In [74]:
image_features = model.encode_image(image).detach().cpu()
text_features = model.encode_text(text).detach().cpu()
print("Image", image_features.shape)
print("Text", text_features.shape)
Image torch.Size([1, 512])
Text torch.Size([5, 512])

Чтобы сравнить вектора признаков изображений и текстов, нам нужна какая-то метрика расстояния между ними. Удобно использовать косинусное расстояние.

$ \large \displaystyle cosine\_similarity = cos(\theta) = \frac{a \cdot b }{|a||b|} $

Если вектора имеют единичную длину, то косинус угла между ними равен скалярному произведению :

$\large cosine\_similarity = a \cdot b $

Эмбеддинги на выходе CLIP не нормализованы:

In [75]:
print(np.linalg.norm(image_features[0]))
10.16952

Нормализуем их:

In [76]:
from torch.nn.functional import normalize

image_features = normalize(image_features)
text_features = normalize(text_features)

print(np.linalg.norm(image_features[0].cpu()))
print(np.linalg.norm(text_features.cpu(), axis=1))
0.99999994
[1. 1. 1. 1. 1.]

Теперь мы можем посчитать скалярное произведение вектора признаков для каждого текста с картинкой и понять, насколько они похожи:

In [77]:
similarities = []
for t in text_features:
    sim = torch.dot(image_features[0], t)
    similarities.append(sim.item())
print(similarities)
[0.3043336272239685, 0.18330001831054688, 0.25699421763420105, 0.1374691277742386, 0.153982475399971]

Аналогичный результат получится при матричном умножении признаков:

In [78]:
sims = torch.matmul(text_features, image_features.T)
print(sims.detach().cpu().tolist())
[[0.3043336570262909], [0.18330001831054688], [0.25699421763420105], [0.1374691277742386], [0.1539824903011322]]

Разрыв уже не такой внушительный, как после softmax:

In [79]:
plt.figure(figsize=(6, 3))
plt.bar(range(len(similarities)), similarities)
plt.show()

Но можно умножить расстояния на константу перед отправкой в softmax:

In [80]:
chilled_sims = sims.flatten() * 100

И тогда результат совпадет с тем, что выдал CLIP:

In [81]:
s = chilled_sims.softmax(dim=0).numpy()
print(s)
plt.figure(figsize=(6, 4))
plt.bar(range(len(s)), s)
plt.show()
[9.9127930e-01 5.4925276e-06 8.7150587e-03 5.6151642e-08 2.9277098e-07]

Классификация ImageNet¶

Создадим из названий классов простые текстовые описания и напечатаем 10 штук для примера:

In [82]:
descriptions = []
for val in imagenet_labels.values():
    name = val[1].replace("_", " ")
    descriptions.append(f"a photo of {name}")
print(descriptions[0:10])
['a photo of tench', 'a photo of goldfish', 'a photo of great white shark', 'a photo of tiger shark', 'a photo of hammerhead', 'a photo of electric ray', 'a photo of stingray', 'a photo of cock', 'a photo of hen', 'a photo of ostrich']
In [83]:
import clip

img, label = microImgNet[0]

model, preprocess = clip.load("ViT-B/32", device=device)


for i in range(6):
    img, label = microImgNet[i * 6]
    name = microImgNet.labels[i * 6][1]

    image = preprocess(img).unsqueeze(0).to(device)
    text = clip.tokenize(descriptions).to(device)

    with torch.no_grad():
        image_features = model.encode_image(image)
        text_features = model.encode_text(text)

        logits_per_image, logits_per_text = model(image, text)
        probs = logits_per_image.softmax(dim=-1).cpu().numpy()
        class_num = probs.argmax()
        descr = descriptions[class_num]
        show(img, descr, i)

Практические рекомендации¶

  • Используйте EfficientNet качестве baseline
  • Для доступа к новым моделям используйте Hugging Face
  • Не учите трансформеры с нуля

Список использованной литературы

AlexNet

ImageNet Classification with Deep Convolutional Neural Networks (Krizhevsky et al., 2012)

Подробнее про AlexNet

ZFNet

Visualizing and Understanding Convolutional Networks (Zeiler et al., 2013)

VGG

Very Deep Convolutional Networks for Large-Scale Image Recognition (Simonyan et al., 2014)

Краткое описание VGGNet

GoogLeNet

Going Deeper with Convolutions (Szegedy et al., 2014)

Global Average Pooling

Network In Network, Lin et al., 2013

Блог-пост про Global Average Pooling

ResNet

Deep Residual Learning for Image Recognition (He et al., 2015)

Fixup Initialization

Fixup Initialization: Residual Learning Without Normalization (Zhang et. al, 2019)

ResNeXt

Aggregated Residual Transformations for Deep Neural Networks (Xie et al., 2016)

Review of ResNet Family: from ResNet to ResNeSt

DenseNet

Densely Connected Convolutional Networks (Huang et al., 2016)

WideResNet

Wide Residual Networks (Zagoruyko et al., 2016)

SENet

Squeeze-and-Excitation Networks (Hu et al., 2017)

Channel Attention and Squeeze-and-Excitation Networks (SENet)

MobileNet

MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications (Howard et al., 2017)

EfficientNet

EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks (Tan et al., 2019)

Visual Transformer

Visual Transformers: Token-based Image Representation and Processing for Computer Vision (Wu et al., 2020)

Squeeze-and-Excitation Networks (Hu et al., 2019))

An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale (Dosovitskiy et al., 2020)

MLP-Mixer

MLP-Mixer: An all-MLP Architecture for Vision (Tolstikhin et al., 2021)

Разбор во влоге Yannic Kilcher

Блог-пост про MLP-Mixer

Разное

Блог-пост про Grouped Convolution

Benchmark Analysis of Representative Deep Neural Network Architectures (Bianco et al., 2018)

Neural Architecture Search with Reinforcement Learning (Zoph et al., 2016)