Архитектуры CNN
Мы рассмотрели базовые компоненты, из которых состоят современные свёрточные нейронные сети, а также техники их обучения.
На этом занятии рассмотрим, какие модели можно построить на основе этих компонентов.
Large Scale Visual Recognition Challenge
ImageNet — один из самых известных проектов в области распознавания изображений и машинного зрения. Содержит более 14 миллионов вручную размеченных изображений, принадлежащих к 1000 классам.
Для загрузки с официального сайта необходимо запрашивать доступ, но можно загрузить данные с Kaggle. Однако архив занимает порядка 156Gb и не поместится на диск Colab. Поэтому воспользуемся другим репозиторием, в котором находится 1000 изображений из оригинального датасета.
P.S. Для загрузки данных, которые стали недоступны на официальных сайтах, можно использовать Academic Torrents. В частности для ImageNet.
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")
# 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
Загрузили категории:
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']}
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()
Посмотрим на картинки:
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)
На каждом изображении может быть один или несколько предметов, относящихся к одному из 1000 классов. Для метрики Тop5 алгоритм выдает метки 5 классов. Если предмет, относящийся к одному из этих классов, есть на изображении, то ответ засчитывается как верный. Для Top1, соответственно, принимается только метка одного класса.
Создатели: Alex Krizhevsky, Ilya Sutskever, Geoffrey E. Hinton University of Toronto
Как упоминалось на первой лекции, современный бум нейросетевых технологий начался в 2012 году, когда AlexNet с большим отрывом от конкурентов победила в ImageNet.
В AlexNet есть все компоненты, которые мы рассматривали ранее. Её архитектура состоит из пяти свёрточных слоёв, между которыми располагаются pooling-слои и слои нормализации, а завершают нейросеть три полносвязных слоя.
Можно заметить, что нейросеть состоит из двух работающих параллельно нейросетей, которые обмениваются информацией после 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:
Сравним PyTorch реализацию с оригинальной. В чем отличия?
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]
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 версии:
Количество слоев и размеры сверток те же.
Проверим, как работает:
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)
Visualizing and Understanding Convolutional Networks (Zeiler et al., 2013)
Тюнингованный AlexNet
Нейросеть ZFNet, созданная учеными из Йорского университета, в 2013 году выиграла соревнования, достигнув результата 11.7%. В ней AlexNet использовалась в качестве основы, но с изменёнными параметрами и слоями.
Отличия от AlexNet небольшие:
Количество слоев и общая структура сети, когда слои свёртки и пулинга чередуются друг с другом, а затем идут два полносвязных слоя, сохранились.
Very Deep Convolutional Networks for Large-Scale Image Recognition (Simonyan et al., 2014)
Karen Simonyan and Andrew Zisserman (Visual Geometry Group — Oxford)
В 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:
Особенности архитектуры VGGNet:
Появление "стандартных" блоков внутри модели — важное нововведение. Идея базового блока внутри сети будет достаточно широко использоваться дальше.
Разберем на коде:
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" :
Рассмотрим один сверточной слой (см. картинку выше).
$\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}).$$Оценка памяти для хранения параметров слоя:
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
Рецептивное поле — участок входной карты признаков (входного тензора), который при прохождении одного или нескольких слоев нейронной сети формирует один признак на выходе (одно число выходного тензора). Рецептивное поле можно назвать “полем зрения”.
Интуитивно кажется, что чем больше “поле зрение”, тем лучше обобщающая способность свертки. Авторы VGG решили отказаться от свертки 5x5 и заменить ее двумя свертками 3x3.
Рассмотрим рецептивное поле свертки 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 дает ряд преимуществ:
2 маленьких фильтра работают как один большой или даже лучше!
Аналогично 3 свертки 3x3 могут заменить одну свертку 7x7.
Благодаря такой экономии получилось сделать большую по тем временам сеть (16 слоев). Тем не менее, несмотря на применённые способы уменьшения вычислительной сложности и снижение числа параметров, сеть все равно получилась огромной (16-слойная версия сети VGG расходует в 25 раз больше дорогой памяти GPU, нежели AlexNet).
Основная часть памяти расходуется на большие свёртки в начальных слоях, где пространственные размеры (ширина и высота) велики;
Больше всего весов в полносвязных слоях;
Вычислительные ресурсы нужны в первую очередь для сверток.
VGG-16 получилась существенно больше по сравнению с и так довольно объемной AlexNet, и тем более по сравнению с современными моделями.
В значительной степени с этим связано дальнейшее направление развития моделей. В следующем году ImageNet выиграла сеть под названием GoogLeNet.
!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:
!pip install -q GPUtil
Preparing metadata (setup.py) ... done Building wheel for GPUtil (setup.py) ... done
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 можно использовать.
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
Очистка памяти:
torch.cuda.empty_cache()
gpu_usage()
GPU RAM Free: 4682MB | Used: 10419MB | Util 68% | Total 15360MB
input_random = None # del input
out = None # del out
gpu_usage()
GPU RAM Free: 4682MB | Used: 10419MB | Util 68% | Total 15360MB
torch.cuda.empty_cache()
gpu_usage()
GPU RAM Free: 6076MB | Used: 9025MB | Util 59% | Total 15360MB
vgg19 = None
gpu_usage()
GPU RAM Free: 6076MB | Used: 9025MB | Util 59% | Total 15360MB
torch.cuda.empty_cache()
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. Это позволяет более эффективно преобразовывать признаки.
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) )
Свёртку 1х1 можно сравнить с линейным слоем полносвязной нейронной сети. Мы берем вектор из карты признаков (столбик 1x1x64 на картинке) и домножаем на матрицу весов (одну для всех векторов, в данном случае матрица будет 64x32, т.к. она составлена из 32 фильтров с размерами 1x1x64), чтобы получить вектор на выходе (столбик 1x1x32 на картинке).
Это одновременно формирует более сложные признаки, собирая информацию с различных сверток и Max Pooling, и позволяет сократить их количество.
Количество параметров уменьшается в два с лишним раза по сравнению с наивной реализацией. Сеть получается значительно экономичнее.
Использование таких модулей и отсутствие полносвязных слоёв делают GoogLeNet очень эффективной и достаточно точной сетью. Но это далеко не все нововведения, которые появились в этой модели.
В составе GoogLeNet есть небольшая подсеть — Stem Network. Она состоит из трёх свёрточных слоёв (первый с большим фильтом) с двумя pooling-слоями и располагается в самом начале архитектуры. Цель этой подсети — быстро и сильно уменьшить пространственные размеры (сжать изображение перед параллельной обработкой), чтобы минимизировать количество элементов в слоях.
Отдельного внимания заслуживает завершающая часть сети. В AlexNet и VGGNet мы привыкли видеть в конце сети вытягивание карты признаков в вектор и два полносвязных слоя. В GoogLeNet один из полносвязных слоев заменен на Global Average Pooling.
Этот слой просто усредняет все значения в канале. На выходе получаем вектор признаков количество элементов в котором равно количеству каналов.
Плюсы GAP:
В PyTorch можно реализовать GAP при помощи класса nn.AdaptiveAvgPool2d. При создании экземпляра класса указываем, какую пространственную размерность мы хотим получить на выходе. При output_size = 1
получим результат как на иллюстрации.
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])
Блог-пост про 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 уменьшает переобучение, т.к. мы избавляемся от влияния менее важных признаков.
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: дополнительный классификатор
Помимо основного классификатора на выходе сети добавлены два дополнительных классификатора, встроенных в промежуточные слои. Они понадобились для того, чтобы улучшить обратное распространение градиента, потому что без батч-нормализации в таких глубоких сетях градиент очень быстро затухал, и обучить сеть такого размера было серьёзной проблемой.
Обучение VGG осуществлялось непростым способом: сначала обучали 7 слоев, затем добавляли туда следующие и обучали это вручную. Без использования батч-нормализации вряд ли получится повторить результат.
Google подошел более системно, добавив дополнительные выходы, которые способствовали тому, чтобы градиент меньше затухал. Благодаря этому удалось решить серьёзную на тот момент проблему, которая ограничивала возможность обучения глубоких моделей. Статья про батч-нормализацию появилась как раз в 15-ом году, видимо, уже после выхода этой модели.
В 2015 году вышла статья Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift. Благодаря добавлению слоя BatchNorm стало технически возможно обучать сети состоящие из десятков слоев.
Но точность с увеличением глубины не росла:
На слайде выше приведено сравнение графиков обучения 56-слойной и 20-слойной сетей Microsoft, построенных на принципах VGGNet. Как видно из графиков, у 56-слойной сети и на тренировочном, и на тестовом датасете ошибка больше, чем у 20-слойной. Казалось бы, сеть, состоящая из большего количества слоёв, должна работать как минимум не хуже, чем сеть меньшего размера. К сожалению, проблема затухания градиента в этом случае не позволяет эффективно обучить более глубокую сеть.
В 2015 году соревнования выиграла сеть ResNet, архитектура которой предлагала новый подход к решению проблемы обучения глубоких сетей. Она состояла из 152 слоёв и снизила процент ошибок до 3,57%. Это сделало её почти в два раза эффективнее GoogLeNet.
При обучении нейронной сети методом обратного распространения ошибки модуль градиента постепенно уменьшается, проходя через каждый из слоёв сети. В глубоких сетях "длина" такого пути оказывается достаточной для того, чтобы модуль градиента стал мал и процесс обучения фактически остановился. В архитектуре ResNet такая проблема решается отказом от простого последовательного соединения слоёв (stacking) в пользу создания дополнительных связей в вычислительном графе нейронной сети, через которые градиент смог бы распространяться, минуя свёрточные слои и таким образом не затухая.
Сеть архитектуры ResNet состоит из набора так называемых Residual Block-ов. В данном блоке тензор входных признаков пропускается через пару последовательно соединённых свёрточных слоёв, после чего полученный результат складывается поканально с этим же неизменённым входным тензором. Свёрточные слои в таком блоке аппроксимируют не саму функциональную зависимость между входным и выходным тензором, а разность (анг. residual) между такой искомой зависимостью и тождественным преобразованием.
Идея Residual Block состоит в уточнении набора признаков на каждом блоке. Вместо того, чтобы перезаписывать признаки, мы добавляем к выходу предыдущего блока уточнение, сформированное на этом блоке.
Бонусы Residual Block:
Из таких блоков можно построить очень глубокую сеть (были эксперименты из 1000 слоев). Для решения конкретной задачи — победы на ImageNet — хватило 150 слоев (добавление большего количества блоков уже не давало прироста точности).
В ResNet используются многие идеи, которые присутствовали в предыдущих моделях:
Блоки состоят из конструкций, изображенных выше:
Stage ratio
— частота, с которой уменьшаются пространственные размеров карты признаков. Обычно записывается как последовательность чисел, например, [2,2,2,2]
.
Эта запись означает, что в модели 8 базовых блоков и downsampling происходит на каждом втором, что соответствует 18-слойной модели из статьи:
Соответственно для ResNet-34 stage ratio будет $[3,4,6,3]$, для ResNet-50 — $[3,4,6,3]$, а для ResNet-101 — $[3,4,23,3]$.
Код базового блока
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, уменьшающая пространственные размеры карты признаков.
Вот как инициализируются блоки (в первом сжатие происходит, во втором — нет).
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) ) )
Стоит отметить, что сеть ResNet-152 имеет существенно меньше обучаемых параметров, чем VGG-19 (58 миллионов против 144 миллионов).
Это достигнуто за счёт того, что в более глубоких сетях, помимо упомянутых Residual Block-ов с двумя свёрточными слоями с ядром 3х3, применялся более эффективный блок — bottleneck, состоящий из:
При обучении ResNet шаг обучения понижали вручную, когда точность выходила на плато.
Помимо того, что ResNet c огромным отрывом выиграла ImageNet у моделей прошлого года, она стала первой моделью, превысившей точность человеческой разметки. Решения на базе этой архитектуры также стали победителями на соревнованиях по детектированию и сегментации.
Можно взглянуть на архитектуру ResNet под таким углом: на первых слоях мы получаем базовую карту признаков, а на последующих улучшаем ее, прибавляя к ней новые уточненные признаки.
В AlexNet выходы первого слоя обрабатывались параллельно: половина каналов отправлялось на одну видеокарту а половина — на другую.
Только на последних слоях полученные признаки объединялись.
Можно рассматривать такой подход как ансамблирование. Но как минимум он позволяет выполнять часть операций параллельно и таким образом ускорить вычисления.
Эта идея реализуется при помощи 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$ — количество групп.
Возможность вычисления груповой свертки в PyTorch заложена в объект nn.Conv2d
. Количество групп задается параметром groups
, значение которого по умолчанию равно 1.
Сравним время, которое требуется на обычную свертку с groups = 1
и groups = 64
при вычислениях на CPU:
# 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:
# 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. Это связано с уменьшением количества вычислительных операций.
Очистим память:
input_random = None
out = None
2016 Aggregated Residual Transformations for Deep Neural Networks (Xie et al., 2016)
Следующая модель — ResNeXt. Эта сеть выиграла ImageNet в следующем году. Идея немного напоминает блок Inception в GoogLeNet: обрабатывать не сразу все каналы, а распараллелить обработку на несколько групп.
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 — архитектура нейронной сети, специально созданная для работы на CPU мобильных устройств. Высокая скорость работы первой версии MobileNet достигалась благодаря разделимой по глубине свертке (Depthwise Separable Convolution).
Также для данной архитектуры предусматривается возможность варьировать размер входного изображения и ширину слоев.
Функцией активации у MobileNet обычно выступает ReLU6 (ограничение сверху у данной функции активации призвано облегчить дальнейшую 8 битную квантизацию нейронной сети).
В 2018 году была предложена архитектура MobileNetV2, которая существенно превосходит первую версию благодаря добавлению к архитектуре инвертированных остаточных блоков (Inverted Residual Block).
А в 2019 году была предложена уже MobileNetV3, которая была получена при помощи автоматического поиска архитектуры (Network Architecture Search) и дополнительно включала в себя модули squeeze-and-excitation и немонотонную функцию активации swish ("жесткая" версия которой, h-swish, так же призвана облегчить квантизацию).
Давайте разберемся, как все эти улучшения работают.
Обычная свертка в CNN одновременно работает с шириной, высотой и глубиной (каналами) карты признаков. Разделимая по глубине свёртка (Depthwise separable convolution) разделяет вычисления на два этапа:
Применение разделимой по глубине свертки позволяет уменьшить количество обучаемых параметров и вычислительных операций при небольшой потере точности.
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) напоминает классический residual block из архитектуры ResNet, однако имеет ряд существенных отличий. Дело в том, что классический residual block, из-за резкого уменьшения размерности пространства в комбинации с функцией активации ReLU, которая уничтожает всю информацию от отрицательных значений, приводит к потере большого количества информации. Поэтому вместо уменьшения количества слоев в середине, оно, наоборот, увеличивается. Увеличение вычислительной сложности компенсируется использованием разделимой по глубине свертки. А на входе и выходе из блока (где количество слоев уменьшается) отсутствует нелинейность (ReLU).
Картинка из статьи 2018 года (не самая свежая), но она позволяет визуально сравнить модели по трем параметрам:
Здесь можно увидеть, что VGGNet — огромные по объёму модели, но по нынешним меркам они обладают средней точностью. Они требуют больших вычислительных ресурсов, поэтому сейчас их имеет смысл использовать разве что в учебных целях, а модели на базе ResNet (ResNet-50, ResNet-152) довольно хороши: в плане точности какого-то большого отрыва от них здесь не видно. Но, тем не менее, есть модели, которые работают чуть лучше. Рассмотрим их кратко, чтобы было понимание того, куда двигалась мысль в этой области.
Densely Connected Convolutional Networks (Huang et al., 2016)
Еще один вариант, что можно сделать со слоями — добавить дополнительные связи в обход сверточных блоков, чтобы градиент проходил ещё лучше.
Можно также заменить сумму на конкатенацию. Чтобы не увеличить при этому глубину карт признаков, можно использовать свёртки 1x1.
На этих двух принципах построен DenseNet. С точки зрения ресурсов он чуть более требовательный, чем базовый ResNet, и немного более точный.
Wide Residual Networks (Zagoruyko et al., 2016)
Другой подход состоит в том, чтобы увеличивать не глубину, а ширину модели. Авторы утверждают, что ширина (количество фильтров) residuals блока — значительно более важный фактор, чем глубина нейронной сети. 50-слойный Wide ResNet показывает лучшие результаты, чем оригинальный 152-х слойный ResNet. С точки зрения вычислительных ресурсов, использование ширины вместо глубины позволяет более эффективно параллелить вычисления на GPU (parallelizable).
При обычной свёртке значения всех каналов учитывались одинаково. Идея SE блока состоит в том, чтобы взвесить каналы, умножив каждый на свой коэффициент из интервала $[0 \dots 1]$.
Но эти коэффициенты должны зависеть от входных данных, тогда это поможет учитывать контекст. Очевидно, что найти шишку рядом с ёлкой более вероятно, чем рядом с компьютером.
Squeeze-and-Excitation Networks (Hu et al., 2017)
Channel Attention and Squeeze-and-Excitation Networks (SENet)
Основным элементом CNN являются фильтры, которые детектируют группы пикселей, обладающих локальной связностью (рецептивное поле). Сверточный фильтр объединяет:
Он не отделяет эти два типа информации друг от друга. За счет чередования сверточных слоев и операций субдискретизации (pooling) CNN способны получать представления изображений (image representation), которые распознают сложные иерархические паттерны.
Архитектуры сверточных нейронных сетей, рассмотренные нами до этого, концентрировались на поиске лучшего представления изображения за счет улучшения способов поиска зависимости между признаками в пространстве (Inception module, Residual block и т.д.), не затрагивая отношения между каналами.
SENet (Squeeze-and-Excitation Networks) — архитектура нейронной сети, одержавшая победу в ILSVRS-2017. Создатели SENet предложили новую архитектуру блока, называемую Squeeze-and-Excitation (SE-блок), целью которой является поиск лучшего представления изображения за счет моделирования взаимодействия между каналами. Идея состоит в том, что не все каналы одинаково важны, поэтому мы можем выборочно выделять из них более информативные и подавлять менее информативные, создав механизм взвешивания каналов (feature recalibration). SE-блок состоит из следующих процессов:
"Сжатие" (squeeze) каждого канала до единственного числового значения с использованием global pooling. Эта процедура позволяет получить некое глобальное представление результата обработки исходного изображения, сделанного каждым из сверточных фильтров, (global information embedding).
"Возбуждение" (excitation) использует информацию, полученную на этапе "сжатия", для определения взаимодействий между каналами. Для этого используются два полносвязных слоя, первый из которых вводит "узкое место" (bottleneck), уменьшающее размерность в соответствии с параметром сжатия r, а второй восстанавливает размерность до исходной. В результате этой операции получается набор активаций, использующийся для взвешивания соответствующих каналов исходного изображения (adaptive recalibration).
Таким образом, SE-блок использует механизм, идейно напоминающий self-attention, для каналов, чьи отношения не ограничены локальным рецептивным полем соответствующих сверточных фильтров.
Описанный SE-блок может быть интегрирован в современные архитектуры сверточных нейронных сетей, например, входя в состав остаточного блока сети ResNet или Inception модуля, как изображено на рисунке.
## 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 with Reinforcement Learning (Zoph et al., 2016)
Проектировать архитектуры нейронных сетей сложно — можно автоматизировать поиск архитектуры и пойти пить чай!
Для создания MobileNetV3 использовалась система автоматического поиска архитектуры. Вот некоторые ее особенности:
EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks (Tan et al., 2019)
Идея EfficientNet состоит в том, чтобы получить возможность подбирать оптимальную глубину (число слоев), ширину (число каналов в слое) и разрешение (длину и ширину карты признаков) для конкретной задачи. Например, если мы берём входное изображение больше, чем обычно (например, 1024x1024 вместо привычных 256x256), то сети потребуется больше слоёв для увеличения рецептивного поля и больше каналов для захвата более тонких деталей на большом изображении.
Масштабирование происходит с помощью составного коэффициента (compound coefficient).
Например, если у нас есть возможность использовать в $2^N$ больше вычислительных ресурсов, то мы можем просто увеличить глубину сети на $\alpha^N$, ширину — на $\beta^N$, и размер изображения — на $\gamma^N$, где $\alpha$, $\beta$ и $\gamma$ — постоянные коэффициенты, определяемые grid search на исходной немасштабированной модели.
Базовая сеть EfficientNet основана на инвертированных узких остаточных блоках MobileNet в дополнение к блокам сжатия и возбуждения (squeeze-and-excitation blocks).
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.
Squeeze-and-Excitation Networks (Hu et al., 2019)
Vision Transformer — это модель для классификации изображений, которая использует архитектуру трансформера. Попробуем разобраться, как она работает.
В 2020 году стали появляться работы, где модели на базе архитектур трансформер смогли показать результаты лучше, чем у CNN моделей.
BiT — это baseline модель на базе ResNet, ViT — Visual Transformer
Авторы практически полностью отказались от использования сверток, заменив их слоями self-attention. Попробуем понять, почему это сработало.
Добавляя в модель свёрточный слой, мы руководствуемся резонным предположением: чем ближе пиксели на изображении, тем больше будет их взаимное влияние.
В большинстве случаев это работает:
На слое n (красный) активируются нейроны, которые реагируют на морду и на хвост кота.
В карте активаций их выходы оказываются рядом, и в слое n + 1 (синий) они попадают в одну свертку, которая активируется на объектах типа "кот".
Так случается часто, но не всегда:
На этом изображении активации нейронов, реагирующих на морду и хвост, не попадут в одну свертку на следующем слое. Это может привести к тому, что нейрон, обучившийся реагировать на кошек, не активируется.
Причиной этого является допущение (Inductive bias) о взаимном влиянии соседних пикселей.
Self-attention слой лишен этого недостатка. Он обучается оценивать взаимное влияние входов друг на друга. Но как применить его к изображениям?
В статье An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale (Dosovitskiy et al., 2020) предлагается разбивать картинки на кусочки (patches) размером $16\times16$ пикселей и подавать их на вход модели.
Проделаем это:
URL = "https://edunet.kea.su/repo/EduNet-web_dependencies/L09/cat.jpeg"
!wget -q $URL -O image.jpg
Преобразуем изображение в тензор, порежем на фрагменты и отобразим их, используя image_grid
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()
На вход модели они поступят в виде вектора:
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. На рисунке выделено красным.
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
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])
Трансформеры работают с последовательностями за счёт механизма внимания (self-attention). И чтобы подать на вход изображение, требуется превратить его в последовательность.
Сделать это можно разными способами, например, составить последовательность из всех пикселей изображения. Её длина $n = H*W$ (высота на ширину).
Сложность вычисления одноголового слоя self-attention $O(n^2 d )$, где $n$ — число токенов и $d$ — размерность входа (embedding) (для любознательных расчеты тут).
То есть для квадратных изображений $(H==W)$ получим $O(H^3 d )$
Такой подход будет очень вычислительно сложен.
Интуитивно понятно, что кодировать каждый пиксель относительно большим 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 блок никак не кодирует позицию элемента на входе, то важная информация потеряется.
Чтобы избежать таких потерь, информацию, кодирующую позицию фрагмента (patch), добавляют к входным данным self-attention слоя в явном виде.
Теперь мы можем грузить наши изображения в Visual Transformer.
Self-attention блок мы разобрали, остальные блоки модели нам знакомы:
MLP (Multi layer perceptron) — Блок из одного или нескольких линейных слоев
Norm — Layer Normalization
Любопытно, что для предсказания класса используется только выход. Он соответствует дополнительному class embedding. Остальные выходы (а для каждего токена в трансформере есть свой выход) отбрасываются за ненадобностью.
В финале этот специальный токен 0* прогоняют через MLP и предсказывают классы.
Попробуем провести аналогию со свертками. Если рассмотреть сверточную сеть без слоев пулинга и других способов изменения пространственных размеров карт признаков, то можно считать такую сеть механизмом постепенно улучшаюшем качество признаков.
Используем пакет ViT PyTorch
!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, ...
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.
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) )
# Load image
!wget -q https://edunet.kea.su/repo/EduNet-web_dependencies/L09/capybara.jpg
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 с переводом индексов в человеческие названия:
И, собственно, переведем индекс в название:
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, как вы могли догадаться, просто нет).
Вряд ли в вашем распоряжении окажется датасет, сравнимый с JFT-300M (300 миллионов изображений), и GPU/TPU ресурсы, необходимые для обучения с нуля (it could be trained using a standard cloud TPUv3 with 8 cores in approximately 30 days)
Поэтому для работы с пользовательскими данными используется техника дообучения ранее обученной модели на пользовательских данных (fine-tuning).
Для практических задач рекомендуем использовать эту реализацию. Авторы предлагают подход, благодаря которому становится возможным обучить модель на стандартном 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.
Разбор этого материала уже не входит в наш курс и рекомендуется к самостоятельному изучению.
Дополнительно:
Training data-efficient image transformers & distillation through attention (Touvron et al., 2021)
Статьи, предшествовавшие появлению ViT:
Для использования ViT с собственными данными рекомендуем не обучать собственную модель с нуля, а использовать уже предобученную.
Рассмотрим этот процесс на примере. Есть предобученный на ImageNet Visual Transformer, например: deit_tiny_patch16_224
И мы хотим использовать ее со своим датасетом, который может сильно отличаться от ImageNet.
Для примера возьмем CIFAR-10.
!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:
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]
Убедимся, что модель запускается. Загрузим изображение:
!wget -q https://edunet.kea.su/repo/EduNet-web_dependencies/L09/capybara.jpg
И подадим его на вход трансформеру:
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])
Чтобы использовать модель с CIFAR-10, нужно поменять количество выходов слоя, отвечающих за классификацию. Так как в CIFAR-10 десять классов, а в ImageNet — тысяча.
Чтобы понять, как получить доступ к последнему слою, выведем структуру модели:
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, именно он отвечает за классификацию.
print(model.head)
Linear(in_features=192, out_features=1000, bias=True)
Заменим его слоем с 10-ю выходами по количеству классов в CIFAR-10.
model.head = torch.nn.Linear(192, 10, bias=True)
Убедимся, что модель не сломалась.
out = model(imagenet_transform(pil).unsqueeze(0))
print(out.shape)
torch.Size([1, 10])
Теперь загрузим CIFAR-10 и проверим, как дообучится модель
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
Проведем стандартный цикл обучения.
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) только последний слой модели, который мы изменили.
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.
@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
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 в их родном размере, мы бы не потеряли никакой информации, но могли бы в разы ускорить обучение.
На первый взгляд, ничего не мешает это сделать: self-attention слой работает с произвольным количеством входов.
Давайте посмотрим, что будет, если подать на вход модели изображение, отличное по размерам от $224\times224$.
Для этого перезагрузим модель:
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:
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
, попробуем просто поменять его:
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$ позиций.
Эмбеддинги для позиций доступны через свойство:
model.pos_embed.data.shape
torch.Size([1, 197, 192])
Теперь нам надо изменить количество pos embeddings так, чтобы оно было равно 5 (количество patch + 1). Возьмем 5 первых:
model.pos_embed.data = model.pos_embed.data[:, :5, :]
out = model(first_img.unsqueeze(0))
print(out.shape)
torch.Size([1, 10])
Заработало!
Теперь обучим модель. Так как изображения стали намного меньше, то мы можем увеличить размер batch и использовать весь датасет. Также будем обучать все слои, а не только последний.
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]
Сильно быстрее. Посмотрим на результат:
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, и двух эпох для этого мало.
Зато теперь мы можем использовать модель с изображениями любого размера.
Авторы продемонстрировали, что сверточная сеть может превзойти трансформер и так же хорошо масштабироваться.
При этом особенных нововведений не потребовалось. Была проведена ревизия процесса обучения. И изменены некоторые параметры модели. За основу был взят обычный ResNet.
Использовали алгоритм обучения такой же как у трансформера:
Macro Design
Свертка $7\times7$ с шагом $2$ в блоке сжатия заменена сверткой $4\times4$ с шагом $4$ как у SWIN Transformer
stage ration поменялся с $(3, 4, 6, 3)$ на $(3, 3, 9, 3)$.
Micro design
Обучается несколько моделей, а затем их веса усредняются. Работает для различных архитектур.
Все пречисленные выше модели учились при помощи классического обучения с учителем: на размеченных данных из ImageNet.
Однако данных как в открытом, так и закрытом доступе с каждым годом становится все больше. Логично предположить, что из этих данных можно извлечь дополнительную информацию, которая поможет лучше классифицировать старый добрый ImageNet.
Модели и алгоритмы, о которых пойдет речь дальше, тем или иным образом используют эти внешние данные.
Knowledge distillation — техника, которая используется для обучения одной модели (маленькой) при помощи другой (большой).
Представим, что у нас нет больше доступа к ImageNet, но на диске осталась модель, которую на нем учили, например Efficientnet_b7. Модель большая и точная, а нам нужно решить некоторую простую задачу, например, отличать котов от собак. Но делать это на мобильном устройстве и очень быстро.
Можно воспользоваться большой моделью, и с ее помощью разметить имеющиеся неразмеченные изображения, а затем на этих данных учить маленькую и быструю модель.
Подход, описанный выше, будет работать, но можно поступить и хитрее.
Предположим, большая сеть классифицирует MNIST, и для цифры:
мы получили вот такой вектор логитов
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()
Самый большой логит у тройки, что верно. Но и у восьмерки он большой, так как эти цифры похожи.
То есть логиты содержат дополнительную полезную информацию.
Когда мы превратим их в вероятности, она сохранится.
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 вектор), эта доп. информация пропадет при округлении.
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)
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()
Чтобы не терять информацию, можно предсказывать не метку, а весь вектор вероятностей, который получается на выходе большой модели .
В процессе выполнения задачи классификации картинок мы получаем карты признаков, которые отвечают за распознавание разных особенностей и паттернов на изображениях. Эту карту признаков мы используем для решения задачи классификации.
Эти же признаки можно использовать для решения других задач. Для этого можно заменить финальные слои нейросети:
На основании карт признаков можно проводить детектирование объектов, сегментацию, генерировать вектора признаков (embedding) для разных задач: сравнения изображений, распознавания лиц, трекинга и т.д.
Часть сети (слева) является достаточно универсальной.
Можно обучить веса, обучая сеть классифицировать изображения, а потом использовать предобученный фрагмент для вычисления карты признаков для любой другой задачи.
Часть нейросети, вычисляющая карту признаков, называется backbone (скелет, основа).
Методика, описанная в статье Bootstrap Your Own Latent, использует дистилляцию для получения качественного представления данных в пространстве признаков (latent space).
Идеи метода:
Если искажения не меняют объект до неузнаваемости, то его класс поменяться не должен, следовательно, можно требовать от модели, чтобы вектора признаков у аугментированных изображений были похожи.
Так как меток у нас нет, то в качестве loss можно использовать расстояние между векторами признаков. Для векторов, полученных из одного изображения, оно должно быть меньше, чем между векторами различных изображений. Эта идея Contrastive Loss, которая подробно будет обсуждаться в 11-й лекции.
При наивной реализации такой подход работает не слишком хорошо, так как общее количество сравнений для датасета с N изображениями и M аугментациями — это $ (N*M)^2$ Поэтому приходится выбирать из всех возможных пар только часть самых сложных (Hard example Mining ).
Создаются две одинаковые сети (online и target) и два различных набора аугментаций (t и t`). В первую сеть подаются изображения, аугментированные при помощи t, во вторую — при помощи t'.
Предсказания моделей сравниваются, и loss подсчитываться как расстояние между предсказаниями, но обновление весов методом градиентного спуска происходит только у одной модели (online).
Веса второй(target) постепенно обновляются как экспоненциальное скользящее среднее от весов первой модели.
$\xi \leftarrow \tau \xi + (1-\tau\theta),$
где $\tau$ — скорость обновления, $\theta$ — веса другой(online) модели.
Затем достаточно получить embedding-и для изображений из ImageNet и классифицировать их при помощи линейного классификатора.
Большая библиотека, где реализован BYOL: документация и репозиторий.
Чтобы не качать зависимости, воспользуемся этим репозиторием.
!pip install -q byol-pytorch
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]
Contrastive Language-Image Pre-training
Для обучения модели был создан новый датасет, включающий 400M пар изображение–текст.
Для использования в качестве классификатора достаточно создать текстовый эмбеддинг для слова, описывающего каждый класс, а затем сравнить его с эмбеддингом изображения.
Точность классификации не дотягивает до SOTA supervised моделей, но зато работает без дообучения на различных датасетах.
Установка:
!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
img, class_num = microImgNet[200]
show(img, microImgNet.labels[200][1], 0)
На вход CLIP надо подать список текстов, на выходе получим оценку того, какой из них больше всего подходит к изображению.
texts = [
"A man with a gasoline saw is getting firewood",
"Santa Claus sleigh",
"chain saw",
"cat",
"dog",
]
Можно использовать разные backbone:
import clip
print(clip.available_models())
['RN50', 'RN101', 'RN50x4', 'RN50x16', 'RN50x64', 'ViT-B/32', 'ViT-B/16', 'ViT-L/14', 'ViT-L/14@336px']
Создаем экземпляр модели:
import clip
model, preprocess = clip.load("ViT-B/32", device="cpu")
100%|███████████████████████████████████████| 338M/338M [00:04<00:00, 76.2MiB/s]
Дополнительно получили объект preprocess
с необходимыми трансформациями. Подготовим с их помощью изображение и токенизируем текст:
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.
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]]
Вот так можно извлечь признаки для картинки и для текста:
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 не нормализованы:
print(np.linalg.norm(image_features[0]))
10.16952
Нормализуем их:
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.]
Теперь мы можем посчитать скалярное произведение вектора признаков для каждого текста с картинкой и понять, насколько они похожи:
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]
Аналогичный результат получится при матричном умножении признаков:
sims = torch.matmul(text_features, image_features.T)
print(sims.detach().cpu().tolist())
[[0.3043336570262909], [0.18330001831054688], [0.25699421763420105], [0.1374691277742386], [0.1539824903011322]]
Разрыв уже не такой внушительный, как после softmax:
plt.figure(figsize=(6, 3))
plt.bar(range(len(similarities)), similarities)
plt.show()
Но можно умножить расстояния на константу перед отправкой в softmax:
chilled_sims = sims.flatten() * 100
И тогда результат совпадет с тем, что выдал CLIP:
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]
Создадим из названий классов простые текстовые описания и напечатаем 10 штук для примера:
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']
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)
Список использованной литературы
AlexNet
ImageNet Classification with Deep Convolutional Neural Networks (Krizhevsky et al., 2012)
ZFNet
Visualizing and Understanding Convolutional Networks (Zeiler et al., 2013)
VGG
Very Deep Convolutional Networks for Large-Scale Image Recognition (Simonyan et al., 2014)
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
EfficientNet
EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks (Tan et al., 2019)
Visual Transformer
Squeeze-and-Excitation Networks (Hu et al., 2019))
MLP-Mixer
MLP-Mixer: An all-MLP Architecture for Vision (Tolstikhin et al., 2021)
Разбор во влоге Yannic Kilcher
Разное
Блог-пост про Grouped Convolution
Benchmark Analysis of Representative Deep Neural Network Architectures (Bianco et al., 2018)
Neural Architecture Search with Reinforcement Learning (Zoph et al., 2016)