.. _sec_conv_layer:
Convolução para Imagens
=======================
Agora que entendemos como as camadas convolucionais funcionam na teoria,
estamos prontos para ver como eles funcionam na prática. Com base na
nossa motivação de redes neurais convolucionais como arquiteturas
eficientes para explorar a estrutura em dados de imagem, usamos imagens
como nosso exemplo de execução.
A Operação de Correlação Cruzada
--------------------------------
Lembre-se de que, estritamente falando, as camadas convolucionais são um
nome impróprio, uma vez que as operações que elas expressam são
descritos com mais precisão como correlações cruzadas. Com base em
nossas descrições de camadas convolucionais em :numref:`sec_why-conv`,
em tal camada, um tensor de entrada e um tensor de *kernel* são
combinados para produzir um tensor de saída por meio de uma operação de
correlação cruzada.
Vamos ignorar os canais por enquanto e ver como isso funciona com dados
bidimensionais e representações ocultas. Em :numref:`fig_correlation`,
a entrada é um tensor bidimensional com altura de 3 e largura de 3.
Marcamos a forma do tensor como :math:`3 \times 3` or (:math:`3`,
:math:`3`). A altura e a largura do *kernel* são 2. A forma da *janela
do kernel* (ou *janela de convolução*) é dada pela altura e largura do
*kernel* (aqui é :math:`2 \times 2`).
.. _fig_correlation:
.. figure:: ../img/correlation.svg
Operação de correlação cruzada bidimensional. 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+1\times1+3\times2+4\times3=19`.
Na operação de correlação cruzada bidimensional, começamos com a janela
de convolução posicionada no canto superior esquerdo do tensor de
entrada e o deslizamos pelo tensor de entrada, ambos da esquerda para a
direita e de cima para baixo. Quando a janela de convolução desliza para
uma determinada posição, o subtensor de entrada contido nessa janela e o
tensor do *kernel* são multiplicados elemento a elemento e o tensor
resultante é resumido produzindo um único valor escalar. Este resultado
fornece o valor do tensor de saída no local correspondente. Aqui, o
tensor de saída tem uma altura de 2 e largura de 2 e os quatro elementos
são derivados de a operação de correlação cruzada bidimensional:
.. math::
0\times0+1\times1+3\times2+4\times3=19,\\
1\times0+2\times1+4\times2+5\times3=25,\\
3\times0+4\times1+6\times2+7\times3=37,\\
4\times0+5\times1+7\times2+8\times3=43.
Observe que ao longo de cada eixo, o tamanho da saída é ligeiramente
menor que o tamanho de entrada. Como o *kernel* tem largura e altura
maiores que um, só podemos calcular corretamente a correlação cruzada
para locais onde o *kernel* se encaixa totalmente na imagem, o tamanho
da saída é dado pelo tamanho da entrada :math:`n_h \times n_w` menos o
tamanho do *kernel* de convolução :math:`k_h \times k_w` através da
.. math:: (n_h-k_h+1) \times (n_w-k_w+1).
Este é o caso, pois precisamos de espaço suficiente para “deslocar” o
*kernel* de convolução na imagem. Mais tarde, veremos como manter o
tamanho inalterado preenchendo a imagem com zeros em torno de seu limite
para que haja espaço suficiente para mudar o *kernel*. Em seguida,
implementamos este processo na função ``corr2d``, que aceita um tensor
de entrada ``X`` e um tensor de *kernel* ``K`` e retorna um tensor de
saída ``Y``.
.. raw:: html
.. raw:: html
.. code:: python
from mxnet import autograd, np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
def corr2d(X, K): #@save
"""Compute 2D cross-correlation."""
h, w = K.shape
Y = np.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
return Y
.. raw:: html
.. raw:: html
.. code:: python
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K): #@save
"""Compute 2D cross-correlation."""
h, w = K.shape
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
return Y
.. raw:: html
.. raw:: html
.. code:: python
import tensorflow as tf
from d2l import tensorflow as d2l
def corr2d(X, K): #@save
"""Compute 2D cross-correlation."""
h, w = K.shape
Y = tf.Variable(tf.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j].assign(tf.reduce_sum(
X[i: i + h, j: j + w] * K))
return Y
.. raw:: html
.. raw:: html
Podemos construir o tensor de entrada ``X`` e o tensor do kernel\ ``K``
from :numref:`fig_correlation` para validar o resultado da
implementação acima da operação de correlação cruzada bidimensional.
.. raw:: html
.. raw:: html
.. code:: python
X = np.array([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = np.array([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)
.. parsed-literal::
:class: output
array([[19., 25.],
[37., 43.]])
.. raw:: html
.. raw:: html
.. code:: python
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)
.. parsed-literal::
:class: output
tensor([[19., 25.],
[37., 43.]])
.. raw:: html
.. raw:: html
.. code:: python
X = tf.constant([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = tf.constant([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Camadas Convolucionais
----------------------
Uma camada convolucional correlaciona a entrada e o *kernel* e adiciona
um *bias* escalar para produzir uma saída. Os dois parâmetros de uma
camada convolucional são o *kernel* e o *bias* escalar. Ao treinar
modelos com base em camadas convolucionais, normalmente inicializamos os
*kernels* aleatoriamente, assim como faríamos com uma camada totalmente
conectada.
Agora estamos prontos para implementar uma camada convolucional
bidimensional com base na função ``corr2d`` definida acima. Na função
construtora ``__init__``, declaramos ``weight`` e\ ``bias`` como os dois
parâmetros do modelo. A função de propagação direta chama a função
``corr2d`` e adiciona o viés.
.. raw:: html
.. raw:: html
.. code:: python
class Conv2D(nn.Block):
def __init__(self, kernel_size, **kwargs):
super().__init__(**kwargs)
self.weight = self.params.get('weight', shape=kernel_size)
self.bias = self.params.get('bias', shape=(1,))
def forward(self, x):
return corr2d(x, self.weight.data()) + self.bias.data()
.. raw:: html
.. raw:: html
.. code:: python
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
self.weight = nn.Parameter(torch.rand(kernel_size))
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
.. raw:: html
.. raw:: html
.. code:: python
class Conv2D(tf.keras.layers.Layer):
def __init__(self):
super().__init__()
def build(self, kernel_size):
initializer = tf.random_normal_initializer()
self.weight = self.add_weight(name='w', shape=kernel_size,
initializer=initializer)
self.bias = self.add_weight(name='b', shape=(1, ),
initializer=initializer)
def call(self, inputs):
return corr2d(inputs, self.weight) + self.bias
.. raw:: html
.. raw:: html
Na convolução :math:`h \times w` ou um *kernel* de convolução
:math:`h \times w` a altura e a largura do *kernel* de convolução são
:math:`h` e :math:`w`, respectivamente. Também nos referimos a uma
camada convolucional com um kernel de convolução :math:`h \times w`
simplesmente como uma camada convolucional :math:`h \times w`
Detecção de Borda de Objeto em Imagens
--------------------------------------
Vamos analisar uma aplicação simples de uma camada convolucional:
detectar a borda de um objeto em uma imagem encontrando a localização da
mudança de pixel. Primeiro, construímos uma “imagem” de
:math:`6\times 8` pixels. As quatro colunas do meio são pretas (0) e as
demais são brancas (1).
.. raw:: html
.. raw:: html
.. code:: python
X = np.ones((6, 8))
X[:, 2:6] = 0
X
.. parsed-literal::
:class: output
array([[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.]])
.. raw:: html
.. raw:: html
.. code:: python
X = torch.ones((6, 8))
X[:, 2:6] = 0
X
.. parsed-literal::
:class: output
tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.]])
.. raw:: html
.. raw:: html
.. code:: python
X = tf.Variable(tf.ones((6, 8)))
X[:, 2:6].assign(tf.zeros(X[:, 2:6].shape))
X
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Em seguida, construímos um kernel ``K`` com uma altura de 1 e uma
largura de 2. Quando realizamos a operação de correlação cruzada com a
entrada, se os elementos horizontalmente adjacentes forem iguais, a
saída é 0. Caso contrário, a saída é diferente de zero.
.. raw:: html
.. raw:: html
.. code:: python
K = np.array([[1.0, -1.0]])
.. raw:: html
.. raw:: html
.. code:: python
K = torch.tensor([[1.0, -1.0]])
.. raw:: html
.. raw:: html
.. code:: python
K = tf.constant([[1.0, -1.0]])
.. raw:: html
.. raw:: html
Estamos prontos para realizar a operação de correlação cruzada com os
argumentos ``X`` (nossa entrada) e\ ``K`` (nosso kernel). Como você pode
ver, detectamos 1 para a borda do branco ao preto e -1 para a borda do
preto ao branco. Todas as outras saídas assumem o valor 0.
.. raw:: html
.. raw:: html
.. code:: python
Y = corr2d(X, K)
Y
.. parsed-literal::
:class: output
array([[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.]])
.. raw:: html
.. raw:: html
.. code:: python
Y = corr2d(X, K)
Y
.. parsed-literal::
:class: output
tensor([[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.]])
.. raw:: html
.. raw:: html
.. code:: python
Y = corr2d(X, K)
Y
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Agora podemos aplicar o kernel à imagem transposta. Como esperado, ele
desaparece. O kernel ``K`` detecta apenas bordas verticais.
.. raw:: html
.. raw:: html
.. code:: python
corr2d(d2l.transpose(X), K)
.. parsed-literal::
:class: output
array([[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]])
.. raw:: html
.. raw:: html
.. code:: python
corr2d(X.t(), K)
.. parsed-literal::
:class: output
tensor([[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]])
.. raw:: html
.. raw:: html
.. code:: python
corr2d(tf.transpose(X), K)
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Aprendendo um Kernel
--------------------
Projetar um detector de borda por diferenças finitas ``[1, -1]`` é legal
se sabemos que é exatamente isso que estamos procurando. No entanto,
quando olhamos para *kernels* maiores, e considere camadas sucessivas de
convoluções, pode ser impossível especificar exatamente o que cada
filtro deve fazer manualmente.
Agora vamos ver se podemos aprender o *kernel* que gerou ``Y`` de\ ``X``
olhando apenas para os pares de entrada–saída. Primeiro construímos uma
camada convolucional e inicializamos seu *kernel* como um tensor
aleatório. A seguir, em cada iteração, usaremos o erro quadrático para
comparar ``Y`` com a saída da camada convolucional. Podemos então
calcular o gradiente para atualizar o *kernel*. Por uma questão de
simplicidade, na sequência nós usamos a classe embutida para camadas
convolucionais bidimensionais e ignorar o *bias*.
.. raw:: html
.. raw:: html
.. code:: python
# Construct a two-dimensional convolutional layer with 1 output channel and a
# kernel of shape (1, 2). For the sake of simplicity, we ignore the bias here
conv2d = nn.Conv2D(1, kernel_size=(1, 2), use_bias=False)
conv2d.initialize()
# The two-dimensional convolutional layer uses four-dimensional input and
# output in the format of (example, channel, height, width), where the batch
# size (number of examples in the batch) and the number of channels are both 1
X = X.reshape(1, 1, 6, 8)
Y = Y.reshape(1, 1, 6, 7)
for i in range(10):
with autograd.record():
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
l.backward()
# Update the kernel
conv2d.weight.data()[:] -= 3e-2 * conv2d.weight.grad()
if (i + 1) % 2 == 0:
print(f'batch {i + 1}, loss {float(l.sum()):.3f}')
.. parsed-literal::
:class: output
batch 2, loss 4.949
batch 4, loss 0.831
batch 6, loss 0.140
batch 8, loss 0.024
batch 10, loss 0.004
[04:01:54] src/base.cc:49: GPU context requested, but no GPUs found.
.. raw:: html
.. raw:: html
.. code:: python
# Construct a two-dimensional convolutional layer with 1 output channel and a
# kernel of shape (1, 2). For the sake of simplicity, we ignore the bias here
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)
# The two-dimensional convolutional layer uses four-dimensional input and
# output in the format of (example channel, height, width), where the batch
# size (number of examples in the batch) and the number of channels are both 1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# Update the kernel
conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'batch {i + 1}, loss {l.sum():.3f}')
.. parsed-literal::
:class: output
batch 2, loss 9.670
batch 4, loss 2.786
batch 6, loss 0.944
batch 8, loss 0.354
batch 10, loss 0.139
.. raw:: html
.. raw:: html
.. code:: python
# Construct a two-dimensional convolutional layer with 1 output channel and a
# kernel of shape (1, 2). For the sake of simplicity, we ignore the bias here
conv2d = tf.keras.layers.Conv2D(1, (1, 2), use_bias=False)
# The two-dimensional convolutional layer uses four-dimensional input and
# output in the format of (example channel, height, width), where the batch
# size (number of examples in the batch) and the number of channels are both 1
X = tf.reshape(X, (1, 6, 8, 1))
Y = tf.reshape(Y, (1, 6, 7, 1))
Y_hat = conv2d(X)
for i in range(10):
with tf.GradientTape(watch_accessed_variables=False) as g:
g.watch(conv2d.weights[0])
Y_hat = conv2d(X)
l = (abs(Y_hat - Y)) ** 2
# Update the kernel
update = tf.multiply(3e-2, g.gradient(l, conv2d.weights[0]))
weights = conv2d.get_weights()
weights[0] = conv2d.weights[0] - update
conv2d.set_weights(weights)
if (i + 1) % 2 == 0:
print(f'batch {i + 1}, loss {tf.reduce_sum(l):.3f}')
.. parsed-literal::
:class: output
batch 2, loss 13.436
batch 4, loss 2.790
batch 6, loss 0.688
batch 8, loss 0.205
batch 10, loss 0.071
.. raw:: html
.. raw:: html
Observe que o erro caiu para um valor pequeno após 10 iterações. Agora
daremos uma olhada no tensor do *kernel* que aprendemos.
.. raw:: html
.. raw:: html
.. code:: python
conv2d.weight.data().reshape((1, 2))
.. parsed-literal::
:class: output
array([[ 0.9895 , -0.9873705]])
.. raw:: html
.. raw:: html
.. code:: python
conv2d.weight.data.reshape((1, 2))
.. parsed-literal::
:class: output
tensor([[ 1.0265, -0.9505]])
.. raw:: html
.. raw:: html
.. code:: python
tf.reshape(conv2d.get_weights()[0], (1, 2))
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
Indeed, the learned kernel tensor is remarkably close to the kernel
tensor ``K`` we defined earlier.
Correlação Cruzada e Convolução
-------------------------------
Lembre-se de nossa observação de :numref:`sec_why-conv` da
correspondência entre as operações de correlação cruzada e convolução.
Aqui, vamos continuar a considerar as camadas convolucionais
bidimensionais. E se essas camadas realizar operações de convolução
estritas conforme definido em :eq:`eq_2d-conv-discrete` em vez de
correlações cruzadas? Para obter a saída da operação de *convolução*
estrita, precisamos apenas inverter o tensor do *kerne*\ l bidimensional
tanto horizontal quanto verticalmente e, em seguida, executar a operação
de *correlação cruzada* com o tensor de entrada.
É digno de nota que, uma vez que os *kernels* são aprendidos a partir de
dados no aprendizado profundo, as saídas das camadas convolucionais
permanecem inalteradas não importa se tais camadas executam as operações
de convolução estrita ou as operações de correlação cruzada.
Para ilustrar isso, suponha que uma camada convolucional execute
*correlação cruzada* e aprenda o *kernel* em
:numref:`fig_correlation`, que é denotado como a matriz
:math:`\mathbf{K}` aqui. Supondo que outras condições permaneçam
inalteradas, quando esta camada executa *convolução* estrita em vez
disso, o *kernel* aprendido :math:`\mathbf{K}'` será o mesmo que
:math:`\mathbf{K}` depois que :math:`\mathbf{K}'` is é invertido
horizontalmente e verticalmente. Quer dizer, quando a camada
convolucional executa *convolução* estrita para a entrada em
:numref:`fig_correlation` e :math:`\mathbf{K}'`, a mesma saída em
:numref:`fig_correlation` (correlação cruzada da entrada e
:math:`\mathbf{K}`) será obtida.
De acordo com a terminologia padrão da literatura de *deep learning*,
continuaremos nos referindo à operação de correlação cruzada como uma
convolução, embora, estritamente falando, seja ligeiramente diferente.
Além do mais, usamos o termo *elemento* para nos referirmos a uma
entrada (ou componente) de qualquer tensor que representa uma
representação de camada ou um *kernel* de convolução.
Mapa de Características e Campo Receptivo
-----------------------------------------
Conforme descrito em :numref:`subsec_why-conv-channels`, a saída da
camada convolucional em :numref:`fig_correlation` às vezes é chamada
de *mapa de características*, pois pode ser considerado como as
representações aprendidas (características) nas dimensões espaciais (por
exemplo, largura e altura) para a camada subsequente. Nas CNNs, para
qualquer elemento :math:`x` de alguma camada, seu *campo receptivo*
refere-se a todos os elementos (de todas as camadas anteriores) que pode
afetar o cálculo de :math:`x` durante a propagação direta. Observe que o
campo receptivo pode ser maior do que o tamanho real da entrada.
Vamos continuar a usar :numref:`fig_correlation` para explicar o campo
receptivo. Dado o *kernel* de convolução :math:`2 \times 2` o campo
receptivo do elemento de saída sombreado (de valor :math:`19`) são os
quatro elementos na parte sombreada da entrada. Agora, vamos denotar a
saída :math:`2 \times 2` como :math:`\mathbf{Y}` e considere uma CNN
mais profunda com uma camada convolucional adicional :math:`2 \times 2`
que leva :math:`\mathbf{Y}` como sua entrada, produzindo um único
elemento :math:`z`. Nesse caso, o campo receptivo de :math:`z` em
:math:`\mathbf{Y}` inclui todos os quatro elementos de
:math:`\mathbf{Y}`, enquanto o campo receptivo na entrada inclui todos
os nove elementos de entrada. Por isso, quando qualquer elemento em um
mapa de recursos precisa de um campo receptivo maior para detectar
recursos de entrada em uma área mais ampla, podemos construir uma rede
mais profunda.
Resumo
------
- O cálculo central de uma camada convolucional bidimensional é uma
operação de correlação cruzada bidimensional. Em sua forma mais
simples, isso executa uma operação de correlação cruzada nos dados de
entrada bidimensionais e no *kernel* e, em seguida, adiciona um
*bias*.
- Podemos projetar um *kernel* para detectar bordas em imagens.
- Podemos aprender os parâmetros do *kernel* a partir de dados.
- Com os *kernels* aprendidos a partir dos dados, as saídas das camadas
convolucionais permanecem inalteradas, independentemente das
operações realizadas por essas camadas (convolução estrita ou
correlação cruzada).
- Quando qualquer elemento em um mapa de características precisa de um
campo receptivo maior para detectar características mais amplas na
entrada, uma rede mais profunda pode ser considerada.
Exercícios
----------
1. Construa uma imagem ``X`` com bordas diagonais.
1. O que acontece se você aplicar o *kernel* ``K`` nesta seção a ele?
2. O que acontece se você transpõe ``X``?
3. O que acontece se você transpõe ``K``?
2. Quando você tenta encontrar automaticamente o gradiente para a classe
``Conv2D`` que criamos, que tipo de mensagem de erro você vê?
3. Como você representa uma operação de correlação cruzada como uma
multiplicação de matriz, alterando os tensores de entrada e *kernel*?
4. Projete alguns *kernels* manualmente.
1. Qual é a forma de um *kernel* para a segunda derivada?
2. Qual é o *kernel* de uma integral?
3. Qual é o tamanho mínimo de um *kernel* para obter uma derivada de
grau :math:`d`?
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
.. raw:: html