.. _sec_padding:
Preenchimento e Saltos
======================
No exemplo anterior de :numref:`fig_correlation`, nossa entrada tinha
altura e largura de 3 e nosso núcleo de convolução tinha altura e
largura de 2, produzindo uma representação de saída com dimensão
:math:`2\times2`. Como generalizamos em :numref:`sec_conv_layer`,
assumindo que a forma de entrada é :math:`n_h\times n_w` e a forma do
kernel de convolução é :math:`k_h\times k_w`, então a forma de saída
será :math:`(n_h-k_h+1) \times (n_w-k_w+1)`. Portanto, a forma de saída
da camada convolucional é determinada pela forma da entrada e a forma do
núcleo de convolução.
Em vários casos, incorporamos técnicas, incluindo preenchimento e
convoluções com saltos, que afetam o tamanho da saída. Como motivação,
note que uma vez que os *kernels* geralmente têm largura e altura
maiores que :math:`1`, depois de aplicar muitas convoluções sucessivas,
tendemos a acabar com resultados que são consideravelmente menor do que
nossa entrada. Se começarmos com uma imagem de :math:`240 \times 240`
pixels, :math:`10` camadas de :math:`5 \times 5` convoluções reduzem a
imagem para :math:`200 \times 200` pixels, cortando :math:`30 \%` da
imagem e com ela obliterando qualquer informação interessante nos
limites da imagem original. *Preenchimento* é a ferramenta mais popular
para lidar com esse problema.
In other cases, we may want to reduce the dimensionality drastically,
e.g., if we find the original input resolution to be unwieldy. *Strided
convolutions* are a popular technique that can help in these instances.
Preenchimento
-------------
Conforme descrito acima, um problema complicado ao aplicar camadas
convolucionais é que tendemos a perder pixels no perímetro de nossa
imagem. Uma vez que normalmente usamos pequenos *kernels*, para qualquer
convolução dada, podemos perder apenas alguns pixels, mas isso pode
somar conforme aplicamos muitas camadas convolucionais sucessivas. Uma
solução direta para este problema é adicionar pixels extras de
preenchimento ao redor do limite de nossa imagem de entrada, aumentando
assim o tamanho efetivo da imagem. Normalmente, definimos os valores dos
pixels extras para zero. Em :numref:`img_conv_pad`, preenchemos uma
entrada :math:`3 \times 3`, aumentando seu tamanho para
:math:`5 \times 5`. A saída correspondente então aumenta para uma matriz
:math:`4 \times 4` As partes sombreadas são o primeiro elemento de
saída, bem como os elementos tensores de entrada e kernel usados para o
cálculo de saída: :math:`0\times0+0\times1+0\times2+0\times3=0`.
.. _img_conv_pad:
.. figure:: ../img/conv-pad.svg
Correlação cruzada bidimensional com preenchimento.
Em geral, se adicionarmos um total de :math:`p_h` linhas de
preenchimento (cerca de metade na parte superior e metade na parte
inferior) e um total de :math:`p_w` colunas de preenchimento (cerca de
metade à esquerda e metade à direita), a forma de saída será
.. math:: (n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1).
Isso significa que a altura e largura da saída aumentará em :math:`p_h`
e :math:`p_w`, respectivamente.
Em muitos casos, queremos definir :math:`p_h=k_h-1` e :math:`p_w=k_w-1`
para dar à entrada e saída a mesma altura e largura. Isso tornará mais
fácil prever a forma de saída de cada camada ao construir a rede.
Supondo que :math:`k_h` seja estranho aqui, vamos preencher
:math:`p_h/2` linhas em ambos os lados da altura. Se :math:`k_h` for
par, uma possibilidade é juntar :math:`\lceil p_h/2\rceil` linhas no
topo da entrada e :math:`\lfloor p_h/2\rfloor` linhas na parte inferior.
Vamos preencher ambos os lados da largura da mesma maneira.
CNNs geralmente usam *kernels* de convolução com valores de altura e
largura ímpares, como 1, 3, 5 ou 7. Escolher tamanhos ímpares de
*kernel* tem o benefício que podemos preservar a dimensionalidade
espacial enquanto preenche com o mesmo número de linhas na parte
superior e inferior, e o mesmo número de colunas à esquerda e à direita.
Além disso, esta prática de usar *kernels* estranhos e preenchimento
para preservar precisamente a dimensionalidade oferece um benefício
administrativo. Para qualquer tensor bidimensional ``X``, quando o
tamanho do *kernel* é estranho e o número de linhas e colunas de
preenchimento em todos os lados são iguais, produzindo uma saída com a
mesma altura e largura da entrada, sabemos que a saída ``Y [i, j]`` é
calculada por correlação cruzada do kernel de entrada e convolução com a
janela centralizada em ``X [i, j]``.
No exemplo a seguir, criamos uma camada convolucional bidimensional com
altura e largura de 3 e aplique 1 pixel de preenchimento em todos os
lados. Dada uma entrada com altura e largura de 8, descobrimos que a
altura e a largura da saída também é 8.
.. raw:: html
.. raw:: html
.. code:: python
from mxnet import np, npx
from mxnet.gluon import nn
npx.set_np()
# For convenience, we define a function to calculate the convolutional layer.
# This function initializes the convolutional layer weights and performs
# corresponding dimensionality elevations and reductions on the input and
# output
def comp_conv2d(conv2d, X):
conv2d.initialize()
# Here (1, 1) indicates that the batch size and the number of channels
# are both 1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# Exclude the first two dimensions that do not interest us: examples and
# channels
return Y.reshape(Y.shape[2:])
# Note that here 1 row or column is padded on either side, so a total of 2
# rows or columns are added
conv2d = nn.Conv2D(1, kernel_size=3, padding=1)
X = np.random.uniform(size=(8, 8))
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
(8, 8)
.. raw:: html
.. raw:: html
.. code:: python
import torch
from torch import nn
# We define a convenience function to calculate the convolutional layer. This
# function initializes the convolutional layer weights and performs
# corresponding dimensionality elevations and reductions on the input and
# output
def comp_conv2d(conv2d, X):
# Here (1, 1) indicates that the batch size and the number of channels
# are both 1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# Exclude the first two dimensions that do not interest us: examples and
# channels
return Y.reshape(Y.shape[2:])
# Note that here 1 row or column is padded on either side, so a total of 2
# rows or columns are added
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
torch.Size([8, 8])
.. raw:: html
.. raw:: html
.. code:: python
import tensorflow as tf
# We define a convenience function to calculate the convolutional layer. This
# function initializes the convolutional layer weights and performs
# corresponding dimensionality elevations and reductions on the input and
# output
def comp_conv2d(conv2d, X):
# Here (1, 1) indicates that the batch size and the number of channels
# are both 1
X = tf.reshape(X, (1, ) + X.shape + (1, ))
Y = conv2d(X)
# Exclude the first two dimensions that do not interest us: examples and
# channels
return tf.reshape(Y, Y.shape[1:3])
# Note that here 1 row or column is padded on either side, so a total of 2
# rows or columns are added
conv2d = tf.keras.layers.Conv2D(1, kernel_size=3, padding='same')
X = tf.random.uniform(shape=(8, 8))
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
TensorShape([8, 8])
.. raw:: html
.. raw:: html
Quando a altura e largura do núcleo de convolução são diferentes,
podemos fazer com que a saída e a entrada tenham a mesma altura e
largura definindo diferentes números de preenchimento para altura e
largura.
.. raw:: html
.. raw:: html
.. code:: python
# Here, we use a convolution kernel with a height of 5 and a width of 3. The
# padding numbers on either side of the height and width are 2 and 1,
# respectively
conv2d = nn.Conv2D(1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
(8, 8)
.. raw:: html
.. raw:: html
.. code:: python
# Here, we use a convolution kernel with a height of 5 and a width of 3. The
# padding numbers on either side of the height and width are 2 and 1,
# respectively
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
torch.Size([8, 8])
.. raw:: html
.. raw:: html
.. code:: python
# Here, we use a convolution kernel with a height of 5 and a width of 3. The
# padding numbers on either side of the height and width are 2 and 1,
# respectively
conv2d = tf.keras.layers.Conv2D(1, kernel_size=(5, 3), padding='same')
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
TensorShape([8, 8])
.. raw:: html
.. raw:: html
Saltos
------
Ao calcular a correlação cruzada, começamos com a janela de convolução
no canto superior esquerdo do tensor de entrada, e o deslizamos sobre
todos os locais para baixo e para a direita. Nos exemplos anteriores,
optamos por deslizar um elemento de cada vez. No entanto, às vezes, seja
para eficiência computacional ou porque desejamos reduzir a resolução,
movemos nossa janela mais de um elemento por vez, pulando os locais
intermediários.
Nos referimos ao número de linhas e colunas percorridas por slide como o
*salto*. Até agora, usamos saltos de 1, tanto para altura quanto para
largura. Às vezes, podemos querer dar um salto maior.
:numref:`img_conv_stride` mostra uma operação de correlação cruzada
bidimensional com um salto de 3 na vertical e 2 na horizontal. As partes
sombreadas são os elementos de saída, bem como os elementos tensores de
entrada e *kernel* usados para o cálculo de saída:
:math:`0\times0+0\times1+1\times2+2\times3=8`,
:math:`0\times0+6\times1+0\times2+0\times3=6`. Podemos ver que quando o
segundo elemento da primeira coluna é gerado, a janela de convolução
desliza três fileiras para baixo. A janela de convolução desliza duas
colunas para a direita quando o segundo elemento da primeira linha é
gerado. Quando a janela de convolução continua a deslizar duas colunas
para a direita na entrada, não há saída porque o elemento de entrada não
pode preencher a janela (a menos que adicionemos outra coluna de
preenchimento).
.. _img_conv_stride:
.. figure:: ../img/conv-stride.svg
Correlação cruzada com passos de 3 e 2 para altura e largura,
respectivamente.
Em geral, quando o salto para a altura é :math:`s_h` e a distância para
a largura é :math:`s_w`, a forma de saída é
.. math:: \lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor.
Se definirmos :math:`p_h=k_h-1` e :math:`p_w=k_w-1`, então a forma de
saída será simplificada para
:math:`\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor`.
Indo um passo adiante, se a altura e largura de entrada são divisíveis
pelos saltos na altura e largura, então a forma de saída será
:math:`(n_h/s_h) \times (n_w/s_w)`.
Abaixo, definimos os saltos de altura e largura para 2, reduzindo assim
pela metade a altura e a largura da entrada.
.. raw:: html
.. raw:: html
.. code:: python
conv2d = nn.Conv2D(1, kernel_size=3, padding=1, strides=2)
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
(4, 4)
.. raw:: html
.. raw:: html
.. code:: python
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
torch.Size([4, 4])
.. raw:: html
.. raw:: html
.. code:: python
conv2d = tf.keras.layers.Conv2D(1, kernel_size=3, padding='same', strides=2)
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
TensorShape([4, 4])
.. raw:: html
.. raw:: html
A seguir, veremos um exemplo um pouco mais complicado.
.. raw:: html
.. raw:: html
.. code:: python
conv2d = nn.Conv2D(1, kernel_size=(3, 5), padding=(0, 1), strides=(3, 4))
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
(2, 2)
.. raw:: html
.. raw:: html
.. code:: python
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
torch.Size([2, 2])
.. raw:: html
.. raw:: html
.. code:: python
conv2d = tf.keras.layers.Conv2D(1, kernel_size=(3,5), padding='valid',
strides=(3, 4))
comp_conv2d(conv2d, X).shape
.. parsed-literal::
:class: output
TensorShape([2, 1])
.. raw:: html
.. raw:: html
Por uma questão de brevidade, quando o número de preenchimento em ambos
os lados da altura e largura de entrada são :math:`p_h` e\ :math:`p_w`
respectivamente, chamamos o preenchimento :math:`(p_h, p_w)`.
Especificamente, quando :math:`p_h = p_w = p`, o preenchimento é
:math:`p`. Quando os saltos de altura e largura são :math:`s_h` e
:math:`s_w`, respectivamente, chamamos o salto de :math:`(s_h, s_w)`.
Especificamente, quando :math:`s_h = s_w = s`, , o salto é :math:`s`.
Por padrão, o preenchimento é 0 e a salto é 1. Na prática, raramente
usamos saltos não homogêneos ou preenchimento, ou seja, geralmente temos
:math:`p_h = p_w` e :math:`s_h = s_w`.
Resumo
------
- O preenchimento pode aumentar a altura e a largura da saída. Isso
geralmente é usado para dar à saída a mesma altura e largura da
entrada.
- Os saltos podem reduzir a resolução da saída, por exemplo, reduzindo
a altura e largura da saída para apenas :math:`1/n` da altura e
largura da entrada (:math:`n` é um número inteiro maior que
:math:`1`).
- Preenchimento e saltos podem ser usados para ajustar a
dimensionalidade dos dados de forma eficaz.
Exercises
---------
1. Para o último exemplo nesta seção, use matemática para calcular a
forma de saída para ver se é consistente com o resultado
experimental.
2. Experimente outras combinações de preenchimento e saltos nos
experimentos desta seção.
3. Para sinais de áudio, a que corresponde um salto de 2?
4. Quais são os benefícios computacionais de uma salto maior que 1?
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
.. raw:: html