.. _sec_fcn: Полностью свёрточные сети ========================= Как обсуждалось в :numref:`sec_semantic_segmentation`, семантическая сегментация выполняет классификацию на уровне пикселей. Полностью свёрточная сеть (FCN) использует свёрточную нейронную сеть для преобразования пикселей изображения в классы пикселей :cite:`Long.Shelhamer.Darrell.2015`. В отличие от свёрточных нейронных сетей, с которыми мы сталкивались ранее при решении задач классификации изображений или обнаружения объектов, полностью свёрточная сеть восстанавливает высоту и ширину промежуточных карт признаков до размеров входного изображения: это достигается с помощью транспонированного свёрточного слоя, описанного в :numref:`sec_transposed_conv`. В результате между выходными данными классификации и входным изображением устанавливается взаимно однозначное соответствие на уровне пикселей: размерность канала в любом выходном пикселе содержит результаты классификации для входного пикселя, находящегося в той же пространственной позиции. .. raw:: latex \diilbookstyleinputcell .. code:: python %matplotlib inline import torch import torchvision from torch import nn from torch.nn import functional as F from d2l import torch as d2l Модель ------ Здесь мы описываем базовую архитектуру модели полностью свёрточной сети. Как показано на рисунке :numref:`fig_fcn`, эта модель сначала использует свёрточную нейронную сеть для извлечения признаков изображения, затем преобразует количество каналов в количество классов с помощью свёрточного слоя :math:`1\times 1`, а после этого изменяет высоту и ширину карт признаков до размеров входного изображения с помощью транспонированной свёртки, описанной в :numref:`sec_transposed_conv`. В результате выход модели имеет те же высоту и ширину, что и входное изображение, при этом выходной канал содержит предсказанные классы для входного пикселя в той же пространственной позиции. .. _fig_fcn: .. figure:: ../img/fcn.svg Полностью свёрточная сеть. Ниже мы **используем модель ResNet-18, предварительно обученную на наборе данных ImageNet, для извлечения признаков изображений**, и обозначаем объект модели как ``pretrained_net``. Последние несколько слоёв этой модели включают слой глобального пулинга и полносвязный слой: они не нужны в полностью свёрточной сети. .. raw:: latex \diilbookstyleinputcell .. code:: python pretrained_net = torchvision.models.resnet18(pretrained=True) list(pretrained_net.children())[-3:] Далее мы создаём экземпляр полностью свёрточной сети ``net``. Он копирует все предварительно обученные слои ResNet-18, за исключением последнего слоя глобального пулинга и полносвязного слоя, которые находятся ближе всего к выходу. .. raw:: latex \diilbookstyleinputcell .. code:: python net = nn.Sequential(*list(pretrained_net.children())[:-2]) При входных данных с высотой и шириной 320 и 480 соответственно прямое распространение ``net`` уменьшает высоту и ширину входа до 1/32 от исходных значений, то есть до 10 и 15. .. raw:: latex \diilbookstyleinputcell .. code:: python X = torch.rand(size=(1, 3, 320, 480)) net(X).shape Далее мы используем свёрточный слой :math:`1\times 1` для преобразования числа выходных каналов в число классов (21) набора данных Pascal VOC2012. Наконец, нам необходимо увеличить высоту и ширину карт признаков в 32 раза, чтобы вернуть им высоту и ширину входного изображения. Вспомним, как вычисляется форма выхода свёрточного слоя, в :numref:`sec_padding`. Поскольку :math:`(320-64+16\times2+32)/32=10` и :math:`(480-64+16\times2+32)/32=15`, мы создаём транспонированный свёрточный слой со шагом :math:`32`, задавая высоту и ширину ядра равными :math:`64`, а дополнение — :math:`16`. В общем случае видно, что для шага :math:`s`, дополнения :math:`s/2` (предполагая, что :math:`s/2` является целым числом) и высоты и ширины ядра :math:`2s` транспонированная свёртка увеличит высоту и ширину входа в :math:`s` раз. .. raw:: latex \diilbookstyleinputcell .. code:: python num_classes = 21 net.add_module('final_conv', nn.Conv2d(512, num_classes, kernel_size=1)) net.add_module('transpose_conv', nn.ConvTranspose2d(num_classes, num_classes, kernel_size=64, padding=16, stride=32)) Инициализация слоёв транспонированной свёртки --------------------------------------------- Мы уже знаем, что слои транспонированной свёртки могут увеличивать высоту и ширину карт признаков. В обработке изображений может потребоваться масштабировать изображение, то есть выполнить *апсемплинг*. *Билинейная интерполяция* — одна из наиболее распространённых техник апсемплинга. Она также часто используется для инициализации слоёв транспонированной свёртки. Чтобы объяснить билинейную интерполяцию, рассмотрим случай, когда задано входное изображение и мы хотим вычислить каждый пиксель увеличенного выходного изображения. Для вычисления пикселя выходного изображения с координатой :math:`(x, y)` сначала отобразим координату :math:`(x, y)` на координату :math:`(x', y')` на входном изображении, например, в соответствии с отношением размера входного изображения к размеру выходного. Обратите внимание, что отображенные координаты :math:`x'` и :math:`y'` являются действительными числами. Затем найдем четыре пикселя на входном изображении, ближайшие к координате :math:`(x', y')`. Наконец, пиксель выходного изображения с координатой :math:`(x, y)` вычисляется на основе этих четырёх ближайших пикселей входного изображения и их относительного расстояния до точки :math:`(x', y')`. Апсемплинг с помощью билинейной интерполяции может быть реализован с помощью транспонированного свёрточного слоя, ядро которого формируется с помощью следующей функции ``bilinear_kernel``. Из-за ограничений по объёму мы приводим ниже только реализацию функции ``bilinear_kernel``, не обсуждая вопросы проектирования алгоритма. .. raw:: latex \diilbookstyleinputcell .. code:: python def bilinear_kernel(in_channels, out_channels, kernel_size): factor = (kernel_size + 1) // 2 if kernel_size % 2 == 1: center = factor - 1 else: center = factor - 0.5 og = (torch.arange(kernel_size).reshape(-1, 1), torch.arange(kernel_size).reshape(1, -1)) filt = (1 - torch.abs(og[0] - center) / factor) * \ (1 - torch.abs(og[1] - center) / factor) weight = torch.zeros((in_channels, out_channels, kernel_size, kernel_size)) weight[range(in_channels), range(out_channels), :, :] = filt return weight Давайте проведём эксперимент с повышением разрешения с помощью билинейной интерполяции, которое реализуется с помощью транспонированного свёрточного слоя. Мы создадим транспонированный свёрточный слой, удваивающий высоту и ширину, и инициализируем его ядро с помощью функции ``bilinear_kernel``. .. raw:: latex \diilbookstyleinputcell .. code:: python conv_trans = nn.ConvTranspose2d(3, 3, kernel_size=4, padding=1, stride=2, bias=False) conv_trans.weight.data.copy_(bilinear_kernel(3, 3, 4)); Прочитаем изображение ``X`` и присвоим результат увеличения масштаба переменной ``Y``. Чтобы вывести изображение, нам необходимо изменить положение размерности канала. .. raw:: latex \diilbookstyleinputcell .. code:: python img = torchvision.transforms.ToTensor()(d2l.Image.open('../img/catdog.jpg')) X = img.unsqueeze(0) Y = conv_trans(X) out_img = Y[0].permute(1, 2, 0).detach() Как мы видим, транспонированный свёрточный слой увеличивает высоту и ширину изображения в два раза. За исключением различных масштабов координат, изображение, увеличенное с помощью билинейной интерполяции, и исходное изображение, выведенное на печать в :numref:`sec_bbox`, выглядят одинаково. .. raw:: latex \diilbookstyleinputcell .. code:: python d2l.set_figsize() print('input image shape:', img.permute(1, 2, 0).shape) d2l.plt.imshow(img.permute(1, 2, 0)); print('output image shape:', out_img.shape) d2l.plt.imshow(out_img); В полностью свёрточной сети мы **инициализируем слой транспонированной свёртки с помощью билинейной интерполяции. Для свёрточного слоя :math:`1\times 1` мы используем инициализацию Глорота (Xavier).** .. raw:: latex \diilbookstyleinputcell .. code:: python W = bilinear_kernel(num_classes, num_classes, 64) net.transpose_conv.weight.data.copy_(W); Чтение набора данных -------------------- Мы читаем набор данных для семантической сегментации, как было описано в :numref:`sec_semantic_segmentation`. Форма выходного изображения после случайной обрезки задаётся как :math:`320\times 480`: высота и ширина должны делиться на :math:`32`. .. raw:: latex \diilbookstyleinputcell .. code:: python batch_size, crop_size = 32, (320, 480) train_iter, test_iter = d2l.load_data_voc(batch_size, crop_size) Обучение -------- Теперь мы можем обучить построенную нами полностью свёрточную сеть. Функция потерь и расчёт точности здесь существенно не отличаются от тех, что использовались в задачах классификации изображений в предыдущих главах. Поскольку мы используем выходной канал слоя транспонированной свёртки для предсказания класса каждого пикселя, размерность канала указывается при вычислении функции потерь. Кроме того, точность вычисляется на основе правильности предсказания класса для всех пикселей. .. raw:: latex \diilbookstyleinputcell .. code:: python def loss(inputs, targets): return F.cross_entropy(inputs, targets, reduction='none').mean(1).mean(1) num_epochs, lr, wd, devices = 5, 0.001, 1e-3, d2l.try_all_gpus() trainer = torch.optim.SGD(net.parameters(), lr=lr, weight_decay=wd) d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices) Предсказание ------------ При прогнозировании необходимо стандартизировать входное изображение по каждому каналу и преобразовать его в четырёхмерный формат, требуемый для свёрточной нейронной сети. .. raw:: latex \diilbookstyleinputcell .. code:: python def predict(img): X = test_iter.dataset.normalize_image(img).unsqueeze(0) pred = net(X.to(devices[0])).argmax(dim=1) return pred.reshape(pred.shape[1], pred.shape[2]) Чтобы визуализировать предсказанный класс каждого пикселя, мы сопоставляем предсказанный класс с цветом его метки в наборе данных. .. raw:: latex \diilbookstyleinputcell .. code:: python def label2image(pred): colormap = torch.tensor(d2l.VOC_COLORMAP, device=devices[0]) X = pred.long() return colormap[X, :] Изображения в тестовом наборе данных различаются по размеру и форме. Поскольку модель использует слой транспонированной свёртки с шагом 32, если высота или ширина входного изображения не делятся на 32, высота или ширина выхода транспонированной свёртки будет отличаться от формы входного изображения. Чтобы решить эту проблему, мы можем выделить в изображении несколько прямоугольных областей, высота и ширина которых кратны 32, и выполнить прямое распространение для пикселей в этих областях отдельно. Обратите внимание, что объединение этих прямоугольных областей должно полностью покрывать входное изображение. Если пиксель попадает в несколько прямоугольных областей, среднее значение выходов транспонированной свёртки для этого пикселя из разных областей можно подать на операцию софтмакс для предсказания класса. Для простоты мы прочитаем лишь несколько крупных тестовых изображений и выделим :math:`320\times480` область для предсказания, начиная с верхнего левого угла каждого изображения. Для этих тестовых изображений мы построчно выведем их выделенные области, результаты предсказания и эталонные значения. .. raw:: latex \diilbookstyleinputcell .. code:: python voc_dir = d2l.download_extract('voc2012', 'VOCdevkit/VOC2012') test_images, test_labels = d2l.read_voc_images(voc_dir, False) n, imgs = 4, [] for i in range(n): crop_rect = (0, 0, 320, 480) X = torchvision.transforms.functional.crop(test_images[i], *crop_rect) pred = label2image(predict(X)) imgs += [X.permute(1,2,0), pred.cpu(), torchvision.transforms.functional.crop( test_labels[i], *crop_rect).permute(1,2,0)] d2l.show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n, scale=2); Резюме ------ - Свёрточная сеть сначала использует свёрточную нейронную сеть для извлечения признаков изображения, затем преобразует количество каналов в количество классов с помощью :math:`1\times 1` свёрточного слоя, а затем преобразует высоту и ширину карт признаков в высоту и ширину входного изображения с помощью транспонированной свёртки. - В полностью свёрточной сети мы можем использовать билинейную интерполяцию для инициализации слоя транспонированной свёртки. Упражнения ---------- 1. Если мы используем инициализацию Глорота для транспонированного слоя в эксперименте, как изменится результат? 2. Можно ли дополнительно повысить точность модели, настроив гиперпараметры? 3. Предскажите классы всех пикселей на тестовых изображениях. 4. В оригинальной статье о полностью свёрточных сетях также используются выходы некоторых промежуточных слоёв CNN :cite:`Long.Shelhamer.Darrell.2015`. Попробуйте реализовать эту идею. `Обсуждения `__