6.5. Pooling¶ Open the notebook in SageMaker Studio Lab
Muitas vezes, conforme processamos imagens, queremos gradualmente reduzir a resolução espacial de nossas representações ocultas, agregando informações para que quanto mais alto subimos na rede, maior o campo receptivo (na entrada) ao qual cada nó oculto é sensível.
Muitas vezes, nossa tarefa final faz alguma pergunta global sobre a imagem, por exemplo, contém um gato? Então, normalmente, as unidades de nossa camada final devem ser sensíveis para toda a entrada. Ao agregar informações gradualmente, produzindo mapas cada vez mais grosseiros, alcançamos esse objetivo de, em última análise, aprendendo uma representação global, enquanto mantém todas as vantagens das camadas convolucionais nas camadas intermediárias de processamento.
Além disso, ao detectar recursos de nível inferior, como bordas
(conforme discutido em Section 6.2), frequentemente
queremos que nossas representações sejam um tanto invariáveis à
tradução. Por exemplo, se pegarmos a imagem X
com uma delimitação
nítida entre preto e branco e deslocarmos a imagem inteira em um pixel
para a direita, ou seja, Z [i, j] = X [i, j + 1]
, então a saída para
a nova imagem Z
pode ser muito diferente. A borda terá deslocado um
pixel. Na realidade, os objetos dificilmente ocorrem exatamente no mesmo
lugar. Na verdade, mesmo com um tripé e um objeto estacionário, a
vibração da câmera devido ao movimento do obturador pode mudar tudo em
um pixel ou mais (câmeras de última geração são carregadas com recursos
especiais para resolver esse problema).
Esta seção apresenta camadas de pooling, que servem ao duplo propósito de mitigando a sensibilidade das camadas convolucionais à localização e de representações de downsampling espacialmente.
6.5.1. Pooling Máximo e Pooling Médio¶
Como camadas convolucionais, operadores de pooling consistem em uma janela de formato fixo que é deslizada todas as regiões na entrada de acordo com seu passo, computando uma única saída para cada local percorrido pela janela de formato fixo (também conhecida como janela de pooling). No entanto, ao contrário do cálculo de correlação cruzada das entradas e grãos na camada convolucional, a camada de pooling não contém parâmetros (não há kernel). Em vez disso, os operadores de pooling são determinísticos, normalmente calculando o valor máximo ou médio dos elementos na janela de pooling. Essas operações são chamadas de pooling máximo (pooling máximo para breve) e pooling médio, respectivamente.
Em ambos os casos, como com o operador de correlação cruzada, podemos pensar na janela de pooling começando da parte superior esquerda do tensor de entrada e deslizando pelo tensor de entrada da esquerda para a direita e de cima para baixo. Em cada local que atinge a janela de pooling, ele calcula o máximo ou o médio valor do subtensor de entrada na janela, dependendo se o pooling máximo ou médio é empregado.
Fig. 6.5.1 Pooling máximo com uma forma de janela de pool de \(2\times 2\). As partes sombreadas são o primeiro elemento de saída, bem como os elementos tensores de entrada usados para o cálculo de saída: \(\max(0, 1, 3, 4)=4\).¶
O tensor de saída em Fig. 6.5.1 tem uma altura de 2 e uma largura de 2. Os quatro elementos são derivados do valor máximo em cada janela de pooling:
Uma camada de pooling com uma forma de janela de pool de \(p \times q\) é chamado de camada de pooling \(p \times q\) A operação de pooling é chamada \(p \times q\) pooling.
Vamos retornar ao exemplo de detecção de borda do objeto mencionado no
início desta seção. Agora vamos usar a saída da camada convolucional
como entrada para \(2\times 2\) pooling máximo. Defina a entrada
da camada convolucional como X
e a saída da camada de pooling
comoY
. Se os valores de X [i, j]
e X [i, j + 1]
são ou não
diferentes, ou X [i, j + 1]
e X [i, j + 2]
são diferentes, a
camada de pool sempre produz Y [i, j] = 1
. Ou seja, usando a camada
de pooling máxima \(2\times 2\) ainda podemos detectar se o padrão
reconhecido pela camada convolucional move no máximo um elemento em
altura ou largura.
No código abaixo, implementamos a propagação direta da camada de
pooling na função pool2d
. Esta função é semelhante à função
corr2d
in: numref: sec_conv_layer
. No entanto, aqui não temos
kernel, computando a saída como o máximo ou a média de cada região na
entrada.
from mxnet import np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = np.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
import torch
from torch import nn
from d2l import torch as d2l
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
import tensorflow as tf
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = tf.Variable(tf.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w +1)))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j].assign(tf.reduce_max(X[i: i + p_h, j: j + p_w]))
elif mode =='avg':
Y[i, j].assign(tf.reduce_mean(X[i: i + p_h, j: j + p_w]))
return Y
Podemos construir o tensor de entrada X
em Fig. 6.5.1
para validar a saída da camada de pooling máximo bidimensional.
X = np.array([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
array([[4., 5.],
[7., 8.]])
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
tensor([[4., 5.],
[7., 8.]])
X = tf.constant([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
<tf.Variable 'Variable:0' shape=(2, 2) dtype=float32, numpy=
array([[4., 5.],
[7., 8.]], dtype=float32)>
Além disso, experimentamos a camada de pooling média.
pool2d(X, (2, 2), 'avg')
array([[2., 3.],
[5., 6.]])
pool2d(X, (2, 2), 'avg')
tensor([[2., 3.],
[5., 6.]])
pool2d(X, (2, 2), 'avg')
<tf.Variable 'Variable:0' shape=(2, 2) dtype=float32, numpy=
array([[2., 3.],
[5., 6.]], dtype=float32)>
6.5.2. Preenchimento e Passos¶
Tal como acontece com as camadas convolucionais, camadas de pooling
também podem alterar a forma de saída. E como antes, podemos alterar a
operação para obter uma forma de saída desejada preenchendo a entrada e
ajustando o passo. Podemos demonstrar o uso de preenchimento e passos em
camadas de agrupamento por meio da camada de agrupamento máximo
bidimensional integrada do framework de deep learning. Primeiro
construímos um tensor de entrada X
cuja forma tem quatro dimensões,
onde o número de exemplos (tamanho do lote) e o número de canais são
ambos 1.
X = np.arange(16, dtype=np.float32).reshape((1, 1, 4, 4))
X
array([[[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]]]])
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
X
tensor([[[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]]]])
É importante notar que o tensorflow prefere e é otimizado para as últimas entradas dos canais.
X = tf.reshape(tf.range(16, dtype=tf.float32), (1, 4, 4, 1))
X
<tf.Tensor: shape=(1, 4, 4, 1), dtype=float32, numpy=
array([[[[ 0.],
[ 1.],
[ 2.],
[ 3.]],
[[ 4.],
[ 5.],
[ 6.],
[ 7.]],
[[ 8.],
[ 9.],
[10.],
[11.]],
[[12.],
[13.],
[14.],
[15.]]]], dtype=float32)>
Por padrão, o passo e a janela de pooling na instância da classe
interna do framework têm a mesma forma. Abaixo, usamos uma janela de
pooling de forma (3, 3)
, portanto, obtemos uma forma de passo de
(3, 3)
por padrão.
pool2d = nn.MaxPool2D(3)
# Because there are no model parameters in the pooling layer, we do not need
# to call the parameter initialization function
pool2d(X)
array([[[[10.]]]])
pool2d = nn.MaxPool2d(3)
pool2d(X)
tensor([[[[10.]]]])
pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3])
pool2d(X)
<tf.Tensor: shape=(1, 1, 1, 1), dtype=float32, numpy=array([[[[10.]]]], dtype=float32)>
O passo e o preenchimento podem ser especificados manualmente.
pool2d = nn.MaxPool2D(3, padding=1, strides=2)
pool2d(X)
array([[[[ 5., 7.],
[13., 15.]]]])
Claro, podemos especificar uma janela de pooling retangular arbitrária e especificar o preenchimento e o passo para altura e largura, respectivamente.
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
tensor([[[[ 5., 7.],
[13., 15.]]]])
paddings = tf.constant([[0, 0], [1,0], [1,0], [0,0]])
X_padded = tf.pad(X, paddings, "CONSTANT")
pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3], padding='valid',
strides=2)
pool2d(X_padded)
<tf.Tensor: shape=(1, 2, 2, 1), dtype=float32, numpy=
array([[[[ 5.],
[ 7.]],
[[13.],
[15.]]]], dtype=float32)>
Claro, podemos especificar uma janela de pooling retangular arbitrária
e especificar o preenchimento e o passo para altura e largura,
respectivamente. Para nn.MaxPool2D
, o preenchimento deve ser menor
que a metade do kernel_size. Se a condição não for atendida, podemos
primeiro preenchemos a entrada usando nn.functional.pad
e, em
seguida, o passamos para a camada de pooling.
Claro, podemos especificar uma janela de pooling retangular arbitrária
e especificar o preenchimento e o passo para altura e largura,
respectivamente. No TensorFlow, para implementar um preenchimento de 1
em todo o tensor, uma função projetada para preenchimento deve ser
invocada usando tf.pad
. Isso implementará o preenchimento necessário
e permitirá que o supracitado (3, 3) agrupamento com uma passada (2, 2)
para realizar semelhantes aos do PyTorch e MXNet. Ao preencher desta
forma, a variável embutida padding
deve ser definida como
válida
.
pool2d = nn.MaxPool2D((2, 3), padding=(1, 2), strides=(2, 3))
pool2d(X)
array([[[[ 0., 3.],
[ 8., 11.],
[12., 15.]]]])
X_pad = nn.functional.pad(X, (2, 2, 1, 1))
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3))
pool2d(X_pad)
tensor([[[[ 0., 3.],
[ 8., 11.],
[12., 15.]]]])
paddings = tf.constant([[0, 0], [1, 1], [2, 1], [0, 0]])
X_padded = tf.pad(X, paddings, "CONSTANT")
pool2d = tf.keras.layers.MaxPool2D(pool_size=[2, 3], padding='valid',
strides=(2,3))
pool2d(X_padded)
<tf.Tensor: shape=(1, 3, 2, 1), dtype=float32, numpy=
array([[[[ 0.],
[ 3.]],
[[ 8.],
[11.]],
[[12.],
[15.]]]], dtype=float32)>
6.5.3. Canais Múltiplos¶
Ao processar dados de entrada multicanal, a camada de pooling agrupa
cada canal de entrada separadamente, em vez de somar as entradas nos
canais como em uma camada convolucional. Isso significa que o número de
canais de saída para a camada de pooling é igual ao número de canais
de entrada. Abaixo, vamos concatenar os tensores X
eX + 1
na
dimensão do canal para construir uma entrada com 2 canais.
Observe que isso exigirá um concatenação ao longo da última dimensão do TensorFlow devido à sintaxe dos últimos canais.
X = np.concatenate((X, X + 1), 1)
X
array([[[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]],
[[ 1., 2., 3., 4.],
[ 5., 6., 7., 8.],
[ 9., 10., 11., 12.],
[13., 14., 15., 16.]]]])
X = torch.cat((X, X + 1), 1)
X
tensor([[[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]],
[[ 1., 2., 3., 4.],
[ 5., 6., 7., 8.],
[ 9., 10., 11., 12.],
[13., 14., 15., 16.]]]])
X = tf.concat([X, X + 1], 3) # Concatenate along `dim=3` due to channels-last syntax
Como podemos ver, o número de canais de saída ainda é 2 após o pooling.
pool2d = nn.MaxPool2D(3, padding=1, strides=2)
pool2d(X)
array([[[[ 5., 7.],
[13., 15.]],
[[ 6., 8.],
[14., 16.]]]])
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
tensor([[[[ 5., 7.],
[13., 15.]],
[[ 6., 8.],
[14., 16.]]]])
paddings = tf.constant([[0, 0], [1,0], [1,0], [0,0]])
X_padded = tf.pad(X, paddings, "CONSTANT")
pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3], padding='valid',
strides=2)
pool2d(X_padded)
<tf.Tensor: shape=(1, 2, 2, 2), dtype=float32, numpy=
array([[[[ 5., 6.],
[ 7., 8.]],
[[13., 14.],
[15., 16.]]]], dtype=float32)>
Observe que a saída para o pooling de tensorflow parece à primeira vista ser diferente, no entanto numericamente, os mesmos resultados são apresentados como MXNet e PyTorch. A diferença está na dimensionalidade, e na leitura do a saída verticalmente produz a mesma saída que as outras implementações.
6.5.4. Resumo¶
Pegando os elementos de entrada na janela de agrupamento, a operação de agrupamento máxima atribui o valor máximo como a saída e a operação de agrupamento média atribui o valor médio como a saída.
Um dos principais benefícios de uma camada de pooling é aliviar a sensibilidade excessiva da camada convolucional ao local.
Podemos especificar o preenchimento e a passada para a camada de pooling.
O agrupamento máximo, combinado com uma passada maior do que 1, pode ser usado para reduzir as dimensões espaciais (por exemplo, largura e altura).
O número de canais de saída da camada de pooling é igual ao número de canais de entrada.
6.5.5. Exercícios¶
Você pode implementar o pooling médio como um caso especial de uma camada de convolução? Se sim, faça.
Você pode implementar o pooling máximo como um caso especial de uma camada de convolução? Se for assim, faça.
Qual é o custo computacional da camada de pooling? Suponha que a entrada para a camada de pooling seja do tamanho \(c\times h\times w\), a janela de pooling tem um formato de \(p_h\times p_w\) com um preenchimento de \((p_h, p_w)\) e um passo de \((s_h, s_w)\).
Por que você espera que o pooling máximo e o pooling médio funcionem de maneira diferente?
Precisamos de uma camada mínima de pooling separada? Você pode substituí-la por outra operação?
Existe outra operação entre o pooling médio e máximo que você possa considerar (dica: lembre-se do softmax)? Por que não é tão popular?