Detecção *Single Shot Multibox* (SSD)
=====================================
Nas poucas seções anteriores, apresentamos caixas delimitadoras, caixas
de âncora, detecção de objetos multiescala e conjuntos de dados. Agora,
usaremos esse conhecimento prévio para construir um modelo de detecção
de objetos: detecção multibox de disparo único [*Single Shot Multibox
Detection*] (SSD) :cite:`Liu.Anguelov.Erhan.ea.2016`. Este modelo
rápido e fácil já é amplamente utilizado. Alguns dos conceitos de design
e detalhes de implementação deste modelo também são aplicáveis a outros
modelos de detecção de objetos.
Modelo
------
:numref:`fig_ssd` mostra o design de um modelo SSD. Os principais
componentes do modelo são um bloco de rede básico e vários blocos de
recursos multiescala conectados em série. Aqui, o bloco de rede de base
é usado para as características extras de imagens originais e geralmente
assumem a forma de uma rede neural convolucional profunda. O artigo
sobre SSDs opta por colocar um VGG truncado antes do camada de
classificação :cite:`Liu.Anguelov.Erhan.ea.2016`, mas agora é
comumente substituído pelo ResNet. Podemos projetar uma rede de base
para que ela produza alturas e larguras maiores. Desta forma, mais
caixas de âncora são geradas com base neste mapa de características,
permitindo-nos detectar objetos menores. Em seguida, cada bloco de
feições multiescala reduz a altura e largura do mapa de feições
fornecidas pela camada anterior (por exemplo, pode reduzir os tamanhos
pela metade). Os blocos então usam cada elemento no mapa de recursos
para expandir o campo receptivo na imagem de entrada. Desta forma,
quanto mais próximo um bloco de feições multiescala estiver do topo de
:numref:`fig_ssd` menor será o mapa de feições de saída e menos caixas
de âncora são geradas com base no mapa de feições. Além disso, quanto
mais próximo um bloco de recursos estiver do topo, maior será o campo
receptivo de cada elemento no mapa de recursos e mais adequado será para
detectar objetos maiores. Como o SSD gera diferentes números de caixas
de âncora de tamanhos diferentes com base no bloco de rede de base e
cada bloco de recursos multiescala e, em seguida, prevê como categorias
e deslocamentos (ou seja, caixas delimitadoras previsão) das caixas de
âncora para detectar objetos de tamanhos diferentes, SSD é um modelo de
detecção de objetos multiescala.
.. _fig_ssd:
.. figure:: ../img/ssd.svg
O SSD é composto de um bloco de rede base e vários blocos de recursos
multiescala conectados em série.
A seguir, descreveremos a implementação dos módulos em
:numref:`fig_ssd`. Primeiro, precisamos discutir a implementação da
previsão da categoria e da previsão da caixa delimitadora.
Camada de Previsão da Categoria
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Defina o número de categorias de objeto como :math:`q`. Nesse caso, o
número de categorias de caixa de âncora é :math:`q+1`, com 0 indicando
uma caixa de âncora que contém apenas o fundo. Para uma determinada
escala, defina a altura e a largura do mapa de feições para :math:`h` e
:math:`w`, respectivamente. Se usarmos cada elemento como o centro para
gerar :math:`a` caixas de âncora, precisamos classificar um total de
:math:`hwa` caixas de âncora. Se usarmos uma camada totalmente conectada
(FCN) para a saída, isso provavelmente resultará em um número excessivo
de parâmetros do modelo. Lembre-se de como usamos canais de camada
convolucional para gerar previsões de categoria em :numref:`sec_nin`.
O SSD usa o mesmo método para reduzir a complexidade do modelo.
Especificamente, a camada de predição de categoria usa uma camada
convolucional que mantém a altura e largura de entrada. Assim, a saída e
a entrada têm uma correspondência de um para um com as coordenadas
espaciais ao longo da largura e altura do mapa de características.
Supondo que a saída e a entrada tenham as mesmas coordenadas
:math:`(x, y)`, o canal para as coordenadas :math:`(x, y)` no mapa de
feição de saída contém as previsões de categoria para todas as caixas
âncora geradas usando as coordenadas do mapa de feição de entrada
:math:`(x, y)` como o Centro. Portanto, existem :math:`a(q+1)` canais de
saída, com os canais de saída indexados como :math:`i(q+1)+j`
(:math:`0 \leq j \leq q`) representando as previsões do índice de
categoria :math:`j` para o índice de caixa de âncora :math:`i`.
Agora, vamos definir uma camada de predição de categoria deste tipo.
Depois de especificar os parâmetros :math:`a` e :math:`q`, ele usa uma
camada convolucional :math:`3\times3` com um preenchimento de 1. As
alturas e larguras de entrada e saída dessa camada convolucional
permanecem inalteradas.
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
from mxnet import autograd, gluon, image, init, np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
def cls_predictor(num_anchors, num_classes):
return nn.Conv2D(num_anchors * (num_classes + 1), kernel_size=3,
padding=1)
.. 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
def cls_predictor(num_inputs, num_anchors, num_classes):
return nn.Conv2d(num_inputs, num_anchors * (num_classes + 1),
kernel_size=3, padding=1)
.. raw:: html
.. raw:: html
Camada de Previsão de Caixa Delimitadora
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
O design da camada de previsão da caixa delimitadora é semelhante ao da
camada de previsão da categoria. A única diferença é que, aqui,
precisamos prever 4 deslocamentos para cada caixa de âncora, em vez de
categorias :math:`q+1`.
.. raw:: html
.. raw:: html
.. code:: python
def bbox_predictor(num_anchors):
return nn.Conv2D(num_anchors * 4, kernel_size=3, padding=1)
.. raw:: html
.. raw:: html
.. code:: python
def bbox_predictor(num_inputs, num_anchors):
return nn.Conv2d(num_inputs, num_anchors * 4, kernel_size=3, padding=1)
.. raw:: html
.. raw:: html
Concatenando Previsões para Múltiplas Escalas
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Como mencionamos, o SSD usa mapas de recursos com base em várias escalas
para gerar caixas de âncora e prever suas categorias e deslocamentos.
Como as formas e o número de caixas de âncora centradas no mesmo
elemento diferem para os mapas de recursos de escalas diferentes, as
saídas de predição em escalas diferentes podem ter formas diferentes.
No exemplo a seguir, usamos o mesmo lote de dados para construir mapas
de características de duas escalas diferentes, ``Y1`` e ``Y2``. Aqui,
``Y2`` tem metade da altura e metade da largura de ``Y1``. Usando a
previsão de categoria como exemplo, assumimos que cada elemento nos
mapas de características ``Y1`` e\ ``Y2`` gera cinco (Y1) ou três (Y2)
caixas de âncora. Quando há 10 categorias de objeto, o número de canais
de saída de predição de categoria é :math:`5\times(10+1)=55` ou
:math:`3\times(10+1)=33`. O formato da saída de previsão é (tamanho do
lote, número de canais, altura, largura). Como você pode ver, exceto
pelo tamanho do lote, os tamanhos das outras dimensões são diferentes.
Portanto, devemos transformá-los em um formato consistente e concatenar
as previsões das várias escalas para facilitar o cálculo subsequente.
.. raw:: html
.. raw:: html
.. code:: python
def forward(x, block):
block.initialize()
return block(x)
Y1 = forward(np.zeros((2, 8, 20, 20)), cls_predictor(5, 10))
Y2 = forward(np.zeros((2, 16, 10, 10)), cls_predictor(3, 10))
(Y1.shape, Y2.shape)
.. parsed-literal::
:class: output
((2, 55, 20, 20), (2, 33, 10, 10))
.. raw:: html
.. raw:: html
.. code:: python
def forward(x, block):
return block(x)
Y1 = forward(torch.zeros((2, 8, 20, 20)), cls_predictor(8, 5, 10))
Y2 = forward(torch.zeros((2, 16, 10, 10)), cls_predictor(16, 3, 10))
(Y1.shape, Y2.shape)
.. parsed-literal::
:class: output
(torch.Size([2, 55, 20, 20]), torch.Size([2, 33, 10, 10]))
.. raw:: html
.. raw:: html
A dimensão do canal contém as previsões para todas as caixas de âncora
com o mesmo centro. Primeiro movemos a dimensão do canal para a dimensão
final. Como o tamanho do lote é o mesmo para todas as escalas, podemos
converter os resultados da previsão para o formato binário (tamanho do
lote, altura :math:`\times` largura :math:`\times` número de canais)
para facilitar a concatenação subsequente no :math:`1^{\mathrm{st}}`
dimensão.
.. raw:: html
.. raw:: html
.. code:: python
def flatten_pred(pred):
return npx.batch_flatten(pred.transpose(0, 2, 3, 1))
def concat_preds(preds):
return np.concatenate([flatten_pred(p) for p in preds], axis=1)
.. raw:: html
.. raw:: html
.. code:: python
def flatten_pred(pred):
return torch.flatten(pred.permute(0, 2, 3, 1), start_dim=1)
def concat_preds(preds):
return torch.cat([flatten_pred(p) for p in preds], dim=1)
.. raw:: html
.. raw:: html
Assim, independentemente das diferentes formas de ``Y1`` e\ ``Y2``,
ainda podemos concatenar os resultados da previsão para as duas escalas
diferentes do mesmo lote.
.. raw:: html
.. raw:: html
.. code:: python
concat_preds([Y1, Y2]).shape
.. parsed-literal::
:class: output
(2, 25300)
.. raw:: html
.. raw:: html
.. code:: python
concat_preds([Y1, Y2]).shape
.. parsed-literal::
:class: output
torch.Size([2, 25300])
.. raw:: html
.. raw:: html
Bloco de Redução de Amostragem de Altura e Largura
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Para detecção de objetos multiescala, definimos o seguinte bloco
``down_sample_blk``, que reduz a altura e largura em 50%. Este bloco
consiste em duas camadas convolucionais :math:`3\times3` com um
preenchimento de 1 e uma camada de *pooling* máximo :math:`2\times2` com
uma distância de 2 conectadas em uma série. Como sabemos,
:math:`3\times3` camadas convolucionais com um preenchimento de 1 não
alteram a forma dos mapas de características. No entanto, a camada de
agrupamento subsequente reduz diretamente o tamanho do mapa de feições
pela metade. Como :math:`1\times 2+(3-1)+(3-1)=6`, cada elemento no mapa
de recursos de saída tem um campo receptivo no mapa de recursos de
entrada da forma :math:`6\times6`. Como você pode ver, o bloco de
redução de altura e largura aumenta o campo receptivo de cada elemento
no mapa de recursos de saída.
.. raw:: html
.. raw:: html
.. code:: python
def down_sample_blk(num_channels):
blk = nn.Sequential()
for _ in range(2):
blk.add(nn.Conv2D(num_channels, kernel_size=3, padding=1),
nn.BatchNorm(in_channels=num_channels),
nn.Activation('relu'))
blk.add(nn.MaxPool2D(2))
return blk
.. raw:: html
.. raw:: html
.. code:: python
def down_sample_blk(in_channels, out_channels):
blk = []
for _ in range(2):
blk.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
blk.append(nn.BatchNorm2d(out_channels))
blk.append(nn.ReLU())
in_channels = out_channels
blk.append(nn.MaxPool2d(2))
return nn.Sequential(*blk)
.. raw:: html
.. raw:: html
Ao testar a computação direta no bloco de redução de altura e largura,
podemos ver que ele altera o número de canais de entrada e divide a
altura e a largura pela metade.
.. raw:: html
.. raw:: html
.. code:: python
forward(np.zeros((2, 3, 20, 20)), down_sample_blk(10)).shape
.. parsed-literal::
:class: output
(2, 10, 10, 10)
.. raw:: html
.. raw:: html
.. code:: python
forward(torch.zeros((2, 3, 20, 20)), down_sample_blk(3, 10)).shape
.. parsed-literal::
:class: output
torch.Size([2, 10, 10, 10])
.. raw:: html
.. raw:: html
Bloco de Rede Base
~~~~~~~~~~~~~~~~~~
O bloco de rede básico é usado para extrair recursos das imagens
originais. Para simplificar o cálculo, construiremos uma pequena rede de
base. Essa rede consiste em três blocos de *downsample* de altura e
largura conectados em série, portanto, dobra o número de canais em cada
etapa. Quando inserimos uma imagem original com a forma
:math:`256\times256`, o bloco de rede base produz um mapa de
características com a forma :math:`32 \times 32`.
.. raw:: html
.. raw:: html
.. code:: python
def base_net():
blk = nn.Sequential()
for num_filters in [16, 32, 64]:
blk.add(down_sample_blk(num_filters))
return blk
forward(np.zeros((2, 3, 256, 256)), base_net()).shape
.. parsed-literal::
:class: output
(2, 64, 32, 32)
.. raw:: html
.. raw:: html
.. code:: python
def base_net():
blk = []
num_filters = [3, 16, 32, 64]
for i in range(len(num_filters) - 1):
blk.append(down_sample_blk(num_filters[i], num_filters[i+1]))
return nn.Sequential(*blk)
forward(torch.zeros((2, 3, 256, 256)), base_net()).shape
.. parsed-literal::
:class: output
torch.Size([2, 64, 32, 32])
.. raw:: html
.. raw:: html
O Modelo Completo
~~~~~~~~~~~~~~~~~
O modelo SSD contém um total de cinco módulos. Cada módulo produz um
mapa de recursos usado para gerar caixas de âncora e prever as
categorias e deslocamentos dessas caixas de âncora. O primeiro módulo é
o bloco de rede base, os módulos de dois a quatro são blocos de redução
de amostragem de altura e largura e o quinto módulo é um bloco global
camada de pooling máxima que reduz a altura e largura para 1. Portanto,
os módulos dois a cinco são todos blocos de recursos multiescala
mostrados em :numref:`fig_ssd`.
.. raw:: html
.. raw:: html
.. code:: python
def get_blk(i):
if i == 0:
blk = base_net()
elif i == 4:
blk = nn.GlobalMaxPool2D()
else:
blk = down_sample_blk(128)
return blk
.. raw:: html
.. raw:: html
.. code:: python
def get_blk(i):
if i == 0:
blk = base_net()
elif i == 1:
blk = down_sample_blk(64, 128)
elif i == 4:
blk = nn.AdaptiveMaxPool2d((1,1))
else:
blk = down_sample_blk(128, 128)
return blk
.. raw:: html
.. raw:: html
Agora, vamos definir o processo de computação progressiva para cada
módulo. Em contraste com as redes neurais convolucionais descritas
anteriormente, este módulo não só retorna a saída do mapa de
características ``Y`` por computação convolucional, mas também as caixas
de âncora da escala atual gerada a partir de\ ``Y`` e suas categorias e
deslocamentos previstos.
.. raw:: html
.. raw:: html
.. code:: python
def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor):
Y = blk(X)
anchors = d2l.multibox_prior(Y, sizes=size, ratios=ratio)
cls_preds = cls_predictor(Y)
bbox_preds = bbox_predictor(Y)
return (Y, anchors, cls_preds, bbox_preds)
.. raw:: html
.. raw:: html
.. code:: python
def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor):
Y = blk(X)
anchors = d2l.multibox_prior(Y, sizes=size, ratios=ratio)
cls_preds = cls_predictor(Y)
bbox_preds = bbox_predictor(Y)
return (Y, anchors, cls_preds, bbox_preds)
.. raw:: html
.. raw:: html
Como mencionamos, quanto mais próximo um bloco de recursos multiescala
está do topo em :numref:`fig_ssd`, maiores são os objetos que ele
detecta e maiores são as caixas de âncora que deve gerar. Aqui, primeiro
dividimos o intervalo de 0,2 a 1,05 em cinco partes iguais para
determinar os tamanhos das caixas de âncora menores em escalas
diferentes: 0,2, 0,37, 0,54, etc. Então, de acordo com
:math:`\sqrt{0.2 \times 0.37} = 0.272`,
:math:`\sqrt{0.37 \times 0.54} = 0.447`, e fórmulas semelhantes,
determinamos os tamanhos de caixas de âncora maiores em escalas
diferentes.
.. raw:: html
.. raw:: html
.. code:: python
sizes = [[0.2, 0.272], [0.37, 0.447], [0.54, 0.619], [0.71, 0.79],
[0.88, 0.961]]
ratios = [[1, 2, 0.5]] * 5
num_anchors = len(sizes[0]) + len(ratios[0]) - 1
.. raw:: html
.. raw:: html
.. code:: python
sizes = [[0.2, 0.272], [0.37, 0.447], [0.54, 0.619], [0.71, 0.79],
[0.88, 0.961]]
ratios = [[1, 2, 0.5]] * 5
num_anchors = len(sizes[0]) + len(ratios[0]) - 1
.. raw:: html
.. raw:: html
Agora, podemos definir o modelo completo, ``TinySSD``.
.. raw:: html
.. raw:: html
.. code:: python
class TinySSD(nn.Block):
def __init__(self, num_classes, **kwargs):
super(TinySSD, self).__init__(**kwargs)
self.num_classes = num_classes
for i in range(5):
# The assignment statement is self.blk_i = get_blk(i)
setattr(self, f'blk_{i}', get_blk(i))
setattr(self, f'cls_{i}', cls_predictor(num_anchors, num_classes))
setattr(self, f'bbox_{i}', bbox_predictor(num_anchors))
def forward(self, X):
anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5
for i in range(5):
# getattr(self, 'blk_%d' % i) accesses self.blk_i
X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward(
X, getattr(self, f'blk_{i}'), sizes[i], ratios[i],
getattr(self, f'cls_{i}'), getattr(self, f'bbox_{i}'))
# In the reshape function, 0 indicates that the batch size remains
# unchanged
anchors = np.concatenate(anchors, axis=1)
cls_preds = concat_preds(cls_preds)
cls_preds = cls_preds.reshape(
cls_preds.shape[0], -1, self.num_classes + 1)
bbox_preds = concat_preds(bbox_preds)
return anchors, cls_preds, bbox_preds
.. raw:: html
.. raw:: html
.. code:: python
class TinySSD(nn.Module):
def __init__(self, num_classes, **kwargs):
super(TinySSD, self).__init__(**kwargs)
self.num_classes = num_classes
idx_to_in_channels = [64, 128, 128, 128, 128]
for i in range(5):
# The assignment statement is self.blk_i = get_blk(i)
setattr(self, f'blk_{i}', get_blk(i))
setattr(self, f'cls_{i}', cls_predictor(idx_to_in_channels[i],
num_anchors, num_classes))
setattr(self, f'bbox_{i}', bbox_predictor(idx_to_in_channels[i],
num_anchors))
def forward(self, X):
anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5
for i in range(5):
# getattr(self, 'blk_%d' % i) accesses self.blk_i
X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward(
X, getattr(self, f'blk_{i}'), sizes[i], ratios[i],
getattr(self, f'cls_{i}'), getattr(self, f'bbox_{i}'))
# In the reshape function, 0 indicates that the batch size remains
# unchanged
anchors = torch.cat(anchors, dim=1)
cls_preds = concat_preds(cls_preds)
cls_preds = cls_preds.reshape(
cls_preds.shape[0], -1, self.num_classes + 1)
bbox_preds = concat_preds(bbox_preds)
return anchors, cls_preds, bbox_preds
.. raw:: html
.. raw:: html
Agora criamos uma instância de modelo SSD e a usamos para realizar
cálculos avançados no minibatch de imagem ``X``, que tem uma altura e
largura de 256 pixels. Como verificamos anteriormente, o primeiro módulo
gera um mapa de recursos com a forma :math:`32 \times 32`. Como os
módulos dois a quatro são blocos de redução de altura e largura, o
módulo cinco é uma camada de agrupamento global e cada elemento no mapa
de recursos é usado como o centro para 4 caixas de âncora, um total de
:math:`(32^2 + 16^2 + 8^2 + 4^2 + 1)\times 4 = 5444` caixas de âncora
são geradas para cada imagem nas cinco escalas.
.. raw:: html
.. raw:: html
.. code:: python
net = TinySSD(num_classes=1)
net.initialize()
X = np.zeros((32, 3, 256, 256))
anchors, cls_preds, bbox_preds = net(X)
print('output anchors:', anchors.shape)
print('output class preds:', cls_preds.shape)
print('output bbox preds:', bbox_preds.shape)
.. parsed-literal::
:class: output
output anchors: (1, 5444, 4)
output class preds: (32, 5444, 2)
output bbox preds: (32, 21776)
.. raw:: html
.. raw:: html
.. code:: python
net = TinySSD(num_classes=1)
X = torch.zeros((32, 3, 256, 256))
anchors, cls_preds, bbox_preds = net(X)
print('output anchors:', anchors.shape)
print('output class preds:', cls_preds.shape)
print('output bbox preds:', bbox_preds.shape)
.. parsed-literal::
:class: output
output anchors: torch.Size([1, 5444, 4])
output class preds: torch.Size([32, 5444, 2])
output bbox preds: torch.Size([32, 21776])
.. raw:: html
.. raw:: html
Treinamento
-----------
Agora, vamos explicar, passo a passo, como treinar o modelo SSD para
detecção de objetos.
Leitura e Inicialização de Dados
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lemos o conjunto de dados de detecção de banana que criamos na seção
anterior.
.. raw:: html
.. raw:: html
.. code:: python
batch_size = 32
train_iter, _ = d2l.load_data_bananas(batch_size)
.. parsed-literal::
:class: output
read 1000 training examples
read 100 validation examples
.. raw:: html
.. raw:: html
.. code:: python
batch_size = 32
train_iter, _ = d2l.load_data_bananas(batch_size)
.. parsed-literal::
:class: output
read 1000 training examples
read 100 validation examples
.. raw:: html
.. raw:: html
Existe 1 categoria no conjunto de dados de detecção de banana. Depois de
definir o módulo, precisamos inicializar os parâmetros do modelo e
definir o algoritmo de otimização.
.. raw:: html
.. raw:: html
.. code:: python
device, net = d2l.try_gpu(), TinySSD(num_classes=1)
net.initialize(init=init.Xavier(), ctx=device)
trainer = gluon.Trainer(net.collect_params(), 'sgd',
{'learning_rate': 0.2, 'wd': 5e-4})
.. raw:: html
.. raw:: html
.. code:: python
device, net = d2l.try_gpu(), TinySSD(num_classes=1)
trainer = torch.optim.SGD(net.parameters(), lr=0.2, weight_decay=5e-4)
.. raw:: html
.. raw:: html
Definindo Funções de Perda e Avaliação
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A detecção de objetos está sujeita a dois tipos de perdas. a primeira é
a perda da categoria da caixa de âncora. Para isso, podemos simplesmente
reutilizar a função de perda de entropia cruzada que usamos na
classificação de imagens. A segunda perda é a perda de deslocamento da
caixa de âncora positiva. A previsão de deslocamento é um problema de
normalização. No entanto, aqui, não usamos a perda quadrática
introduzida anteriormente. Em vez disso, usamos a perda de norma
:math:`L_1`, que é o valor absoluto da diferença entre o valor previsto
e o valor verdadeiro. A variável de máscara ``bbox_masks`` remove caixas
de âncora negativas e caixas de âncora de preenchimento do cálculo de
perda. Finalmente, adicionamos a categoria de caixa de âncora e
compensamos as perdas para encontrar a função de perda final para o
modelo.
.. raw:: html
.. raw:: html
.. code:: python
cls_loss = gluon.loss.SoftmaxCrossEntropyLoss()
bbox_loss = gluon.loss.L1Loss()
def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
cls = cls_loss(cls_preds, cls_labels)
bbox = bbox_loss(bbox_preds * bbox_masks, bbox_labels * bbox_masks)
return cls + bbox
.. raw:: html
.. raw:: html
.. code:: python
cls_loss = nn.CrossEntropyLoss(reduction='none')
bbox_loss = nn.L1Loss(reduction='none')
def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
batch_size, num_classes = cls_preds.shape[0], cls_preds.shape[2]
cls = cls_loss(cls_preds.reshape(-1, num_classes),
cls_labels.reshape(-1)).reshape(batch_size, -1).mean(dim=1)
bbox = bbox_loss(bbox_preds * bbox_masks,
bbox_labels * bbox_masks).mean(dim=1)
return cls + bbox
.. raw:: html
.. raw:: html
Podemos usar a taxa de precisão para avaliar os resultados da
classificação. Como usamos a perda de norma :math:`L_1`, usaremos o erro
absoluto médio para avaliar os resultados da previsão da caixa
delimitadora.
.. raw:: html
.. raw:: html
.. code:: python
def cls_eval(cls_preds, cls_labels):
# Because the category prediction results are placed in the final
# dimension, argmax must specify this dimension
return float((cls_preds.argmax(axis=-1).astype(
cls_labels.dtype) == cls_labels).sum())
def bbox_eval(bbox_preds, bbox_labels, bbox_masks):
return float((np.abs((bbox_labels - bbox_preds) * bbox_masks)).sum())
.. raw:: html
.. raw:: html
.. code:: python
def cls_eval(cls_preds, cls_labels):
# Because the category prediction results are placed in the final
# dimension, argmax must specify this dimension
return float((cls_preds.argmax(dim=-1).type(
cls_labels.dtype) == cls_labels).sum())
def bbox_eval(bbox_preds, bbox_labels, bbox_masks):
return float((torch.abs((bbox_labels - bbox_preds) * bbox_masks)).sum())
.. raw:: html
.. raw:: html
Treinando o Modelo
~~~~~~~~~~~~~~~~~~
Durante o treinamento do modelo, devemos gerar caixas de âncora
multiescala (``âncoras``) no processo de computação direta do modelo e
prever a categoria (``cls_preds``) e o deslocamento (``bbox_preds``)
para cada caixa de âncora. Depois, rotulamos a categoria
(``cls_labels``) e o deslocamento (``bbox_labels``) de cada caixa de
âncora gerada com base nas informações do rótulo ``Y``. Finalmente,
calculamos a função de perda usando a categoria predita e rotulada e os
valores de compensação. Para simplificar o código, não avaliamos o
conjunto de dados de treinamento aqui.
.. raw:: html
.. raw:: html
.. code:: python
num_epochs, timer = 20, d2l.Timer()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['class error', 'bbox mae'])
for epoch in range(num_epochs):
# accuracy_sum, mae_sum, num_examples, num_labels
metric = d2l.Accumulator(4)
for features, target in train_iter:
timer.start()
X = features.as_in_ctx(device)
Y = target.as_in_ctx(device)
with autograd.record():
# Generate multiscale anchor boxes and predict the category and
# offset of each
anchors, cls_preds, bbox_preds = net(X)
# Label the category and offset of each anchor box
bbox_labels, bbox_masks, cls_labels = d2l.multibox_target(anchors,
Y)
# Calculate the loss function using the predicted and labeled
# category and offset values
l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels,
bbox_masks)
l.backward()
trainer.step(batch_size)
metric.add(cls_eval(cls_preds, cls_labels), cls_labels.size,
bbox_eval(bbox_preds, bbox_labels, bbox_masks),
bbox_labels.size)
cls_err, bbox_mae = 1 - metric[0] / metric[1], metric[2] / metric[3]
animator.add(epoch + 1, (cls_err, bbox_mae))
print(f'class err {cls_err:.2e}, bbox mae {bbox_mae:.2e}')
print(f'{len(train_iter._dataset) / timer.stop():.1f} examples/sec on '
f'{str(device)}')
.. parsed-literal::
:class: output
class err 3.55e-03, bbox mae 3.76e-03
2766.4 examples/sec on gpu(0)
.. figure:: output_ssd_739e1b_156_1.svg
.. raw:: html
.. raw:: html
.. code:: python
num_epochs, timer = 20, d2l.Timer()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['class error', 'bbox mae'])
net = net.to(device)
for epoch in range(num_epochs):
# accuracy_sum, mae_sum, num_examples, num_labels
metric = d2l.Accumulator(4)
net.train()
for features, target in train_iter:
timer.start()
trainer.zero_grad()
X, Y = features.to(device), target.to(device)
# Generate multiscale anchor boxes and predict the category and
# offset of each
anchors, cls_preds, bbox_preds = net(X)
# Label the category and offset of each anchor box
bbox_labels, bbox_masks, cls_labels = d2l.multibox_target(anchors, Y)
# Calculate the loss function using the predicted and labeled
# category and offset values
l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels,
bbox_masks)
l.mean().backward()
trainer.step()
metric.add(cls_eval(cls_preds, cls_labels), cls_labels.numel(),
bbox_eval(bbox_preds, bbox_labels, bbox_masks),
bbox_labels.numel())
cls_err, bbox_mae = 1 - metric[0] / metric[1], metric[2] / metric[3]
animator.add(epoch + 1, (cls_err, bbox_mae))
print(f'class err {cls_err:.2e}, bbox mae {bbox_mae:.2e}')
print(f'{len(train_iter.dataset) / timer.stop():.1f} examples/sec on '
f'{str(device)}')
.. parsed-literal::
:class: output
class err 3.24e-03, bbox mae 3.03e-03
5130.7 examples/sec on cuda:0
.. figure:: output_ssd_739e1b_159_1.svg
.. raw:: html
.. raw:: html
Predição
--------
Na fase de previsão, queremos detectar todos os objetos de interesse na
imagem. Abaixo, lemos a imagem de teste e transformamos seu tamanho.
Então, nós o convertemos para o formato quadridimensional exigido pela
camada convolucional.
.. raw:: html
.. raw:: html
.. code:: python
img = image.imread('../img/banana.jpg')
feature = image.imresize(img, 256, 256).astype('float32')
X = np.expand_dims(feature.transpose(2, 0, 1), axis=0)
.. raw:: html
.. raw:: html
.. code:: python
X = torchvision.io.read_image('../img/banana.jpg').unsqueeze(0).float()
img = X.squeeze(0).permute(1,2,0).long()
.. raw:: html
.. raw:: html
Usando a função ``multibox_detection``, prevemos as caixas delimitadoras
com base nas caixas de âncora e seus deslocamentos previstos. Em
seguida, usamos a supressão não máxima para remover caixas delimitadoras
semelhantes.
.. raw:: html
.. raw:: html
.. code:: python
def predict(X):
anchors, cls_preds, bbox_preds = net(X.as_in_ctx(device))
cls_probs = npx.softmax(cls_preds).transpose(0, 2, 1)
output = d2l.multibox_detection(cls_probs, bbox_preds, anchors)
idx = [i for i, row in enumerate(output[0]) if row[0] != -1]
return output[0, idx]
output = predict(X)
.. parsed-literal::
:class: output
[06:10:36] src/operator/nn/./cudnn/./cudnn_algoreg-inl.h:97: Running performance tests to find the best convolution algorithm, this can take a while... (set the environment variable MXNET_CUDNN_AUTOTUNE_DEFAULT to 0 to disable)
.. raw:: html
.. raw:: html
.. code:: python
def predict(X):
net.eval()
anchors, cls_preds, bbox_preds = net(X.to(device))
cls_probs = F.softmax(cls_preds, dim=2).permute(0, 2, 1)
output = d2l.multibox_detection(cls_probs, bbox_preds, anchors)
idx = [i for i, row in enumerate(output[0]) if row[0] != -1]
return output[0, idx]
output = predict(X)
.. raw:: html
.. raw:: html
Por fim, pegamos todas as caixas delimitadoras com um nível de confiança
de pelo menos 0,9 e as exibimos como a saída final.
.. raw:: html
.. raw:: html
.. code:: python
def display(img, output, threshold):
d2l.set_figsize((5, 5))
fig = d2l.plt.imshow(img.asnumpy())
for row in output:
score = float(row[1])
if score < threshold:
continue
h, w = img.shape[0:2]
bbox = [row[2:6] * np.array((w, h, w, h), ctx=row.ctx)]
d2l.show_bboxes(fig.axes, bbox, '%.2f' % score, 'w')
display(img, output, threshold=0.9)
.. figure:: output_ssd_739e1b_183_0.svg
.. raw:: html
.. raw:: html
.. code:: python
def display(img, output, threshold):
d2l.set_figsize((5, 5))
fig = d2l.plt.imshow(img)
for row in output:
score = float(row[1])
if score < threshold:
continue
h, w = img.shape[0:2]
bbox = [row[2:6] * torch.tensor((w, h, w, h), device=row.device)]
d2l.show_bboxes(fig.axes, bbox, '%.2f' % score, 'w')
display(img, output.cpu(), threshold=0.9)
.. figure:: output_ssd_739e1b_186_0.svg
.. raw:: html
.. raw:: html
Resumo
------
- SSD é um modelo de detecção de objetos multiescala. Este modelo gera
diferentes números de caixas de âncora de tamanhos diferentes com
base no bloco de rede de base e cada bloco de recursos multiescala e
prevê as categorias e deslocamentos das caixas de âncora para
detectar objetos de tamanhos diferentes.
- Durante o treinamento do modelo SSD, a função de perda é calculada
usando a categoria prevista e rotulada e os valores de deslocamento.
Exercícios
----------
1. Devido a limitações de espaço, ignoramos alguns dos detalhes de
implementação do modelo SSD neste experimento. Você pode melhorar
ainda mais o modelo nas seguintes áreas?
Função de Perda
~~~~~~~~~~~~~~~
A. Para as compensações previstas, substitua :math:`L_1` perda de norma
por :math:`L_1` de perda de regularização. Esta função de perda usa uma
função quadrada em torno de zero para maior suavidade. Esta é a área
regularizada controlada pelo hiperparâmetro :math:`\sigma`:
.. math::
f(x) =
\begin{cases}
(\sigma x)^2/2,& \text{if }|x| < 1/\sigma^2\\
|x|-0.5/\sigma^2,& \text{otherwise}
\end{cases}
Quando :math:`\sigma` é grande, essa perda é semelhante à perda normal
de :math:`L_1`. Quando o valor é pequeno, a função de perda é mais
suave.
.. raw:: html
.. raw:: html
.. code:: python
sigmas = [10, 1, 0.5]
lines = ['-', '--', '-.']
x = np.arange(-2, 2, 0.1)
d2l.set_figsize()
for l, s in zip(lines, sigmas):
y = npx.smooth_l1(x, scalar=s)
d2l.plt.plot(x.asnumpy(), y.asnumpy(), l, label='sigma=%.1f' % s)
d2l.plt.legend();
.. figure:: output_ssd_739e1b_192_0.svg
.. raw:: html
.. raw:: html
.. code:: python
def smooth_l1(data, scalar):
out = []
for i in data:
if abs(i) < 1 / (scalar ** 2):
out.append(((scalar * i) ** 2) / 2)
else:
out.append(abs(i) - 0.5 / (scalar ** 2))
return torch.tensor(out)
sigmas = [10, 1, 0.5]
lines = ['-', '--', '-.']
x = torch.arange(-2, 2, 0.1)
d2l.set_figsize()
for l, s in zip(lines, sigmas):
y = smooth_l1(x, scalar=s)
d2l.plt.plot(x, y, l, label='sigma=%.1f' % s)
d2l.plt.legend();
.. figure:: output_ssd_739e1b_195_0.svg
.. raw:: html
.. raw:: html
No experimento, usamos a perda de entropia cruzada para a previsão da
categoria. Agora, assuma que a probabilidade de predição da categoria
real :math:`j` é :math:`p_j` e a perda de entropia cruzada é
:math:`-\log p_j`. Também podemos usar a perda focal
:cite:`Lin.Goyal.Girshick.ea.2017`. Dados os hiperparâmetros positivos
:math:`\gamma` e :math:`\alpha`, essa perda é definida como:
.. math:: - \alpha (1-p_j)^{\gamma} \log p_j.
Como você pode ver, ao aumentar :math:`\gamma`, podemos efetivamente
reduzir a perda quando a probabilidade de prever a categoria correta for
alta.
.. raw:: html
.. raw:: html
.. code:: python
def focal_loss(gamma, x):
return -(1 - x) ** gamma * np.log(x)
x = np.arange(0.01, 1, 0.01)
for l, gamma in zip(lines, [0, 1, 5]):
y = d2l.plt.plot(x.asnumpy(), focal_loss(gamma, x).asnumpy(), l,
label='gamma=%.1f' % gamma)
d2l.plt.legend();
.. figure:: output_ssd_739e1b_201_0.svg
.. raw:: html
.. raw:: html
.. code:: python
def focal_loss(gamma, x):
return -(1 - x) ** gamma * torch.log(x)
x = torch.arange(0.01, 1, 0.01)
for l, gamma in zip(lines, [0, 1, 5]):
y = d2l.plt.plot(x, focal_loss(gamma, x), l, label='gamma=%.1f' % gamma)
d2l.plt.legend();
.. figure:: output_ssd_739e1b_204_0.svg
.. raw:: html
.. raw:: html
Treinamento e Previsão
~~~~~~~~~~~~~~~~~~~~~~
B. Quando um objeto é relativamente grande em comparação com a imagem, o
modelo normalmente adota um tamanho de imagem de entrada maior.
C. Isso geralmente produz um grande número de caixas de âncora negativas
ao rotular as categorias da caixa de âncora. Podemos amostrar as caixas
de âncora negativas para equilibrar melhor as categorias de dados. Para
fazer isso, podemos definir um parâmetro ``negative_mining_ratio`` na
função ``multibox_target``.
D. Atribuir hiperparâmetros com pesos diferentes para a perda de
categoria da caixa de âncora e a perda de deslocamento da caixa de
âncora positiva na função de perda.
E. Consulte o documento SSD. Quais métodos podem ser usados para avaliar
a precisão dos modelos de detecção de objetos
:cite:`Liu.Anguelov.Erhan.ea.2016`?
.. raw:: html
.. raw:: html
`Discussões `__
.. raw:: html
.. raw:: html
`Discussões `__
.. raw:: html
.. raw:: html
.. raw:: html