.. _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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. 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
.. raw:: html
`Discussões `__
.. raw:: html
.. raw:: html
`Discussões `__
.. raw:: html
.. raw:: html
.. raw:: html