.. _sec_fcn: Redes Totalmente Convolucionais (*Fully Convolutional Networks*, FCN) ===================================================================== Discutimos anteriormente a segmentação semântica usando cada pixel em uma imagem para previsão de categoria. Uma rede totalmente convolucional (FCN) :cite:`Long.Shelhamer.Darrell.2015` usa uma rede neural convolucional para transformar os pixels da imagem em categorias de pixels. Ao contrário das redes neurais convolucionais previamente introduzidas, uma FCN transforma a altura e largura do mapa de recurso da camada intermediária de volta ao tamanho da imagem de entrada por meio do camada de convolução transposta, de modo que as previsões tenham uma correspondência com a imagem de entrada em dimensão espacial (altura e largura). Dado uma posição na dimensão espacial, a saída da dimensão do canal será uma previsão de categoria do pixel correspondente ao local. Primeiro importaremos o pacote ou módulo necessário para o experimento e depois explicaremos a camada de convolução transposta. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python %matplotlib inline from mxnet import gluon, image, init, np, npx from mxnet.gluon import nn from d2l import mxnet as d2l npx.set_np() .. raw:: html
.. raw:: html
.. 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 .. raw:: html
.. raw:: html
Construindo um Modelo --------------------- Aqui, demonstramos o projeto mais básico de um modelo de rede totalmente convolucional. Conforme mostrado em :numref:`fig_fcn`, a rede totalmente convolucional primeiro usa a rede neural convolucional para extrair características da imagem, então transforma o número de canais no número de categorias através da camada de convolução :math:`1\times 1` e, finalmente, transforma a altura e largura do mapa de recursos para o tamanho da imagem de entrada usando a camada de convolução transposta :numref:`sec_transposed_conv`. A saída do modelo tem a mesma altura e largura da imagem de entrada e uma correspondência de um para um nas posições espaciais. O canal de saída final contém a previsão da categoria do pixel da posição espacial correspondente. .. _fig_fcn: .. figure:: ../img/fcn.svg Rede totalmente convolucional. Abaixo, usamos um modelo ResNet-18 pré-treinado no conjunto de dados ImageNet para extrair recursos de imagem e registrar a instância de rede como ``pretrained_net``. Como você pode ver, as duas últimas camadas da variável membro do modelo ``features`` são a camada de agrupamento global médio\ ``GlobalAvgPool2D`` e a camada de nivelamento de exemplo ``Flatten`` O módulo ``output`` contém a camada totalmente conectada usada para saída. Essas camadas não são necessárias para uma rede totalmente convolucional. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python pretrained_net = gluon.model_zoo.vision.resnet18_v2(pretrained=True) pretrained_net.features[-4:], pretrained_net.output .. parsed-literal:: :class: output (HybridSequential( (0): BatchNorm(axis=1, eps=1e-05, momentum=0.9, fix_gamma=False, use_global_stats=False, in_channels=512) (1): Activation(relu) (2): GlobalAvgPool2D(size=(1, 1), stride=(1, 1), padding=(0, 0), ceil_mode=True, global_pool=True, pool_type=avg, layout=NCHW) (3): Flatten ), Dense(512 -> 1000, linear)) .. raw:: html
.. raw:: html
.. code:: python pretrained_net = torchvision.models.resnet18(pretrained=True) pretrained_net.layer4[1], pretrained_net.avgpool, pretrained_net.fc .. parsed-literal:: :class: output (BasicBlock( (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ), AdaptiveAvgPool2d(output_size=(1, 1)), Linear(in_features=512, out_features=1000, bias=True)) .. raw:: html
.. raw:: html
Em seguida, criamos a instância de rede totalmente convolucional ``net``. Ela duplica todas as camadas neurais, exceto as duas últimas camadas da variável membro de instância ``features`` de ``pretrained_net`` e os parâmetros do modelo obtidos após o pré-treinamento. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python net = nn.HybridSequential() for layer in pretrained_net.features[:-2]: net.add(layer) .. raw:: html
.. raw:: html
.. code:: python net = nn.Sequential(*list(pretrained_net.children())[:-2]) .. raw:: html
.. raw:: html
Dada uma entrada de altura e largura de 320 e 480 respectivamente, o cálculo direto de ``net`` reduzirá a altura e largura da entrada para :math:`1/32` do original, ou seja, 10 e 15. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python X = np.random.uniform(size=(1, 3, 320, 480)) net(X).shape .. parsed-literal:: :class: output (1, 512, 10, 15) .. raw:: html
.. raw:: html
.. code:: python X = torch.rand(size=(1, 3, 320, 480)) net(X).shape .. parsed-literal:: :class: output torch.Size([1, 512, 10, 15]) .. raw:: html
.. raw:: html
Em seguida, transformamos o número de canais de saída para o número de categorias de Pascal VOC2012 (21) por meio da camada de convolução :math:`1\times 1`. Finalmente, precisamos ampliar a altura e largura do mapa de feições por um fator de 32 para alterá-los de volta para a altura e largura da imagem de entrada. Lembre-se do cálculo método para a forma de saída da camada de convolução descrita em :numref:`sec_padding`. Porque :math:`(320-64+16\times2+32)/32=10` e :math:`(480-64+16\times2+32)/32=15`, construímos uma camada de convolução transposta com uma distância de 32 e definimos a altura e largura do *kernel* de convolução para 64 e o preenchimento para 16. Não é difícil ver que, se o passo for :math:`s`, o preenchimento é :math:`s/2` (assumindo que :math:`s/2` é um inteiro ), e a altura e largura do *kernel* de convolução são :math:`2s`, o *kernel* de convolução transposto aumentará a altura e a largura da entrada por um fator de :math:`s`. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python num_classes = 21 net.add(nn.Conv2D(num_classes, kernel_size=1), nn.Conv2DTranspose( num_classes, kernel_size=64, padding=16, strides=32)) .. raw:: html
.. raw:: html
.. 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)) .. raw:: html
.. raw:: html
Inicializando a Camada de Convolução Transposta ----------------------------------------------- Já sabemos que a camada de convolução transposta pode ampliar um mapa de feições. No processamento de imagem, às vezes precisamos ampliar a imagem, ou seja, *upsampling*. Existem muitos métodos para aumentar a amostragem e um método comum é a interpolação bilinear. Simplesmente falando, para obter o pixel da imagem de saída nas coordenadas :math:`(x, y)`, as coordenadas são primeiro mapeadas para as coordenadas da imagem de entrada :math:`(x ', y')`. Isso pode ser feito com base na proporção do tamanho de três entradas em relação ao tamanho da saída. Os valores mapeados :math:`x'` e :math:`y'` são geralmente números reais. Então, encontramos os quatro pixels mais próximos da coordenada :math:`(x ', y')` na imagem de entrada. Finalmente, os pixels da imagem de saída nas coordenadas :math:`(x, y)` são calculados com base nesses quatro pixels na imagem de entrada e suas distâncias relativas a :math:`(x ', y')`. O *upsampling* por interpolação bilinear pode ser implementado pela camada de convolução transposta do *kernel* de convolução construído usando a seguinte função ``bilinear_kernel``. Devido a limitações de espaço, fornecemos apenas a implementação da função ``bilinear_kernel`` e não discutiremos os princípios do algoritmo. .. raw:: html
mxnetpytorch
.. raw:: html
.. 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 = (np.arange(kernel_size).reshape(-1, 1), np.arange(kernel_size).reshape(1, -1)) filt = (1 - np.abs(og[0] - center) / factor) * \ (1 - np.abs(og[1] - center) / factor) weight = np.zeros((in_channels, out_channels, kernel_size, kernel_size)) weight[range(in_channels), range(out_channels), :, :] = filt return np.array(weight) .. raw:: html
.. raw:: html
.. 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 .. raw:: html
.. raw:: html
Agora, vamos experimentar com upsampling de interpolação bilinear implementado por camadas de convolução transpostas. Construa uma camada de convolução transposta que amplie a altura e a largura da entrada por um fator de 2 e inicialize seu kernel de convolução com a função ``bilinear_kernel``. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python conv_trans = nn.Conv2DTranspose(3, kernel_size=4, padding=1, strides=2) conv_trans.initialize(init.Constant(bilinear_kernel(3, 3, 4))) .. raw:: html
.. raw:: html
.. 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)); .. raw:: html
.. raw:: html
Leia a imagem ``X`` e registre o resultado do upsampling como\ ``Y``. Para imprimir a imagem, precisamos ajustar a posição da dimensão do canal. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python img = image.imread('../img/catdog.jpg') X = np.expand_dims(img.astype('float32').transpose(2, 0, 1), axis=0) / 255 Y = conv_trans(X) out_img = Y[0].transpose(1, 2, 0) .. raw:: html
.. raw:: html
.. 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() .. raw:: html
.. raw:: html
Como você pode ver, a camada de convolução transposta amplia a altura e largura da imagem em um fator de 2. Vale ressaltar que, além da diferença na escala de coordenadas, a imagem ampliada por interpolação bilinear e a imagem original impressa em :numref:`sec_bbox` tem a mesma aparência. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python d2l.set_figsize() print('input image shape:', img.shape) d2l.plt.imshow(img.asnumpy()); print('output image shape:', out_img.shape) d2l.plt.imshow(out_img.asnumpy()); .. parsed-literal:: :class: output input image shape: (561, 728, 3) output image shape: (1122, 1456, 3) .. figure:: output_fcn_ce3435_75_1.svg .. raw:: html
.. raw:: html
.. 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); .. parsed-literal:: :class: output input image shape: torch.Size([561, 728, 3]) output image shape: torch.Size([1122, 1456, 3]) .. figure:: output_fcn_ce3435_78_1.svg .. raw:: html
.. raw:: html
Em uma rede totalmente convolucional, inicializamos a camada de convolução transposta para interpolação bilinear com upsampled. Para uma camada de convolução :math:`1\times 1`, usamos o Xavier para inicialização aleatória. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python W = bilinear_kernel(num_classes, num_classes, 64) net[-1].initialize(init.Constant(W)) net[-2].initialize(init=init.Xavier()) .. raw:: html
.. raw:: html
.. code:: python W = bilinear_kernel(num_classes, num_classes, 64) net.transpose_conv.weight.data.copy_(W); .. raw:: html
.. raw:: html
Lendo o *Dataset* ----------------- Lemos o *dataset* usando o método descrito na seção anterior. Aqui, especificamos a forma da imagem de saída cortada aleatoriamente como :math:`320\times 480`, portanto, a altura e a largura são divisíveis por 32. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python batch_size, crop_size = 32, (320, 480) train_iter, test_iter = d2l.load_data_voc(batch_size, crop_size) .. parsed-literal:: :class: output read 1114 examples read 1078 examples .. raw:: html
.. raw:: html
.. code:: python batch_size, crop_size = 32, (320, 480) train_iter, test_iter = d2l.load_data_voc(batch_size, crop_size) .. parsed-literal:: :class: output read 1114 examples read 1078 examples .. raw:: html
.. raw:: html
Treinamento ----------- Agora podemos começar a treinar o modelo. A função de perda e o cálculo de precisão aqui não são substancialmente diferentes daqueles usados na classificação de imagens. Como usamos o canal da camada de convolução transposta para prever as categorias de pixels, a opção ``axis = 1`` (dimensão do canal) é especificada em ``SoftmaxCrossEntropyLoss``. Além disso, o modelo calcula a precisão com base em se a categoria de previsão de cada pixel está correta. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python num_epochs, lr, wd, devices = 5, 0.1, 1e-3, d2l.try_all_gpus() loss = gluon.loss.SoftmaxCrossEntropyLoss(axis=1) net.collect_params().reset_ctx(devices) trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr, 'wd': wd}) d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices) .. parsed-literal:: :class: output loss 0.335, train acc 0.890, test acc 0.850 199.0 examples/sec on [gpu(0), gpu(1)] .. figure:: output_fcn_ce3435_102_1.svg .. raw:: html
.. raw:: html
.. 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) .. parsed-literal:: :class: output loss 0.455, train acc 0.860, test acc 0.849 222.9 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)] .. figure:: output_fcn_ce3435_105_1.svg .. raw:: html
.. raw:: html
Predição -------- Durante a previsão, precisamos padronizar a imagem de entrada em cada canal e transformá-los no formato de entrada quadridimensional exigido pela rede neural convolucional. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python def predict(img): X = test_iter._dataset.normalize_image(img) X = np.expand_dims(X.transpose(2, 0, 1), axis=0) pred = net(X.as_in_ctx(devices[0])).argmax(axis=1) return pred.reshape(pred.shape[1], pred.shape[2]) .. raw:: html
.. raw:: html
.. 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:: html
.. raw:: html
Para visualizar as categorias previstas para cada pixel, mapeamos as categorias previstas de volta às suas cores rotuladas no conjunto de dados. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python def label2image(pred): colormap = np.array(d2l.VOC_COLORMAP, ctx=devices[0], dtype='uint8') X = pred.astype('int32') return colormap[X, :] .. raw:: html
.. raw:: html
.. code:: python def label2image(pred): colormap = torch.tensor(d2l.VOC_COLORMAP, device=devices[0]) X = pred.long() return colormap[X, :] .. raw:: html
.. raw:: html
O tamanho e a forma das imagens no conjunto de dados de teste variam. Como o modelo usa uma camada de convolução transposta com uma distância de 32, quando a altura ou largura da imagem de entrada não é divisível por 32, a altura ou largura da saída da camada de convolução transposta se desvia do tamanho da imagem de entrada. Para resolver esse problema, podemos recortar várias áreas retangulares na imagem com alturas e larguras como múltiplos inteiros de 32 e, em seguida, realizar cálculos para a frente nos pixels nessas áreas. Quando combinadas, essas áreas devem cobrir completamente a imagem de entrada. Quando um pixel é coberto por várias áreas, a média da saída da camada de convolução transposta no cálculo direto das diferentes áreas pode ser usada como uma entrada para a operação softmax para prever a categoria. Para simplificar, lemos apenas algumas imagens de teste grandes e recortamos uma área com um formato de :math:`320\times480` no canto superior esquerdo da imagem. Apenas esta área é usada para previsão. Para a imagem de entrada, imprimimos primeiro a área cortada, depois imprimimos o resultado previsto e, por fim, imprimimos a categoria rotulada. .. raw:: html
mxnetpytorch
.. raw:: html
.. 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, 480, 320) X = image.fixed_crop(test_images[i], *crop_rect) pred = label2image(predict(X)) imgs += [X, pred, image.fixed_crop(test_labels[i], *crop_rect)] d2l.show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n, scale=2); .. figure:: output_fcn_ce3435_129_0.svg .. raw:: html
.. raw:: html
.. 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); .. figure:: output_fcn_ce3435_132_0.svg .. raw:: html
.. raw:: html
Resumo ------ - A rede totalmente convolucional primeiro usa a rede neural convolucional para extrair características da imagem, depois transforma o número de canais no número de categorias por meio da camada de convolução :math:`1\times 1` e, finalmente, transforma a altura e largura do mapa de características para o tamanho da imagem de entrada usando a camada de convolução transposta para produzir a categoria de cada pixel. - Em uma rede totalmente convolucional, inicializamos a camada de convolução transposta para interpolação bilinear com *upsampling*. Exercícios ---------- 1. Se usarmos Xavier para inicializar aleatoriamente a camada de convolução transposta, o que acontecerá com o resultado? 2. Você pode melhorar ainda mais a precisão do modelo ajustando os hiperparâmetros? 3. Preveja as categorias de todos os pixels na imagem de teste. 4. As saídas de algumas camadas intermediárias da rede neural convolucional também são usadas no artigo sobre redes totalmente convolucionais :cite:`Long.Shelhamer.Darrell.2015`. Tente implementar essa ideia. .. raw:: html
mxnetpytorch
.. raw:: html
`Discussões `__ .. raw:: html
.. raw:: html
`Discussões `__ .. raw:: html
.. raw:: html
.. raw:: html