10. Convolutional
Uma camada convolucional é uma camada densa à qual duas coisas foram proibidas: ela não pode ligar um pixel a um pixel distante e não pode usar um peso diferente em um lugar diferente. Nada é acrescentado. Capacidade é retirada, de propósito.
Essa é a ideia inteira e vale enunciá-la como uma afirmação sobre o mundo, não sobre código: em uma imagem, o que importa é local e significa a mesma coisa onde quer que apareça. Uma borda no canto superior esquerdo é o mesmo evento que uma borda no canto inferior direito. Uma camada densa não sabe disso e precisa aprender — separadamente, em cada posição, a partir de exemplos. A convolução recebe isso de graça.
O capítulo de deep learning chamou as arquiteturas de priors expressos como restrições sobre os pesos. Este capítulo é essa frase desenvolvida por completo para um caso, seguida da contabilidade honesta de 2026 sobre onde o prior ainda se paga.
A restrição, com o preço na etiqueta
Abaixo há um kernel 3×3 deslizando sobre uma entrada 9×9. Aperte play e observe o que de fato acontece: os mesmos nove números são lidos em cada posição e cada célula de saída é um produto escalar.
A linha embaixo do painel é o argumento. Nove pesos produzem o mapa de ativação inteiro; a camada densa que liga as mesmas entradas às mesmas saídas precisa de milhares — e, o que importa mais do que a contagem de parâmetros, ela teria de redescobrir o detector de bordas em cada posição, a partir de dados que ela não tem.
Dois botões mudam o formato da saída e os dois aparecem o tempo todo na prática:
- Passo (stride) \(s\) — o quanto a janela pula. \(s = 2\) reduz a resolução pela metade e é a forma mais barata de downsampling; é também como o campo receptivo consegue crescer geometricamente (próxima seção).
- Preenchimento (padding) \(p\) — zeros inventados em volta da borda, para que a saída preserve o tamanho da entrada.
padding = (k-1)/2com \(k\) ímpar é o padding "same" e é o padrão que você deve assumir.
Leia o erro de formato, não chute
A esmagadora maioria dos bugs em CNNs é de formato e a fórmula acima resolve todos eles. Quando o PyTorch reclamar no primeiro Linear depois da pilha convolucional, imprima o formato em vez de ajustar o número até parar de reclamar:
Equivariância é o que você ganha; invariância é o que você queria
O compartilhamento de pesos dá uma propriedade precisa e demonstrável: desloque a entrada e o mapa de ativação se desloca na mesma medida. Isso é equivariância à translação e é exatamente o certo para um detector de features.
Não é o que um classificador precisa. Um classificador precisa de invariância — "gato" independentemente de onde o gato esteja. Equivariância só vira invariância quando você colapsa os eixos espaciais e há duas formas honestas de fazer isso:
| Mecanismo | O que de fato faz |
|---|---|
| Pooling / convolução com passo | Descarta a posição dentro da janela. Compra uma invariância pequena e local e recompra resolução em forma de compute. |
| Global average pooling no final | Colapsa \(H \times W\) inteiro. É daqui que a invariância de classificação realmente vem e foi o que substituiu a cabeça totalmente conectada gigante que AlexNet e VGG carregavam. |
| Data augmentation | Recortes, espelhamentos, escalas aleatórias. Compra invariância a tudo aquilo a que a convolução não pode ser equivariante — rotação, escala, cor — mostrando exemplos. |
Convoluções não são invariantes a rotação nem a escala e nunca foram
O prior cobre translação e mais nada. Invariância a rotação, escala e ponto de vista é comprada com augmentation e com dados, numa CNN exatamente como num Transformer. Afirmações de que "CNNs são invariantes a transformações" são um exagero muito difundido de um teorema de uma palavra só.
O campo receptivo é o orçamento de verdade
Uma unidade no fundo da pilha não vê a imagem. Ela vê o que a alimentou, recursivamente. Esse conjunto é seu campo receptivo e o crescimento dele é a restrição que moldou toda arquitetura convolucional já projetada.
Mexa nos controles e três fatos aparecem:
- Com passo 1, o alcance cresce linearmente. Cada camada 3×3 soma 2. Atravessar uma imagem de 224 pixels levaria mais de cem camadas — e é por isso que nenhuma arquitetura é construída assim.
- O passo é o que faz o alcance crescer geometricamente. Cada camada com passo 2 dobra o salto de todas as camadas acima dela. É isto — não o custo computacional — o motivo pelo qual CNNs fazem downsampling: é a única rota acessível ao contexto global.
- A dilatação compra alcance sem pesos e sem downsampling, lendo com buracos. É por isso que convoluções dilatadas dominaram a segmentação semântica, onde se precisa de contexto e de resolução plena6.
Duas camadas 3×3 ganham de uma 5×5 e esse foi o artigo VGG inteiro
As duas alcançam 5 pixels. O par custa \(2 \times 9 = 18\) pesos contra 25 e coloca uma não linearidade no meio. Empilhar kernels pequenos é estritamente melhor e é por isso que 3×3 é o padrão quase universal e por que convoluções de entrada 11×11 sumiram depois de 20143.
A aritmética, num lugar só
Para entrada \(X \in \mathbb{R}^{B \times C_{in} \times H \times W}\) e kernel \(W \in \mathbb{R}^{C_{out} \times C_{in} \times k \times k}\):
A passagem reversa é a mesma operação três vezes — a razão pela qual convoluções são rápidas em hardware construído para um único kernel:
| Gradiente | O que é | Implementação |
|---|---|---|
| \(\partial L / \partial b\) | Soma do gradiente de saída sobre batch e espaço | Uma redução |
| \(\partial L / \partial W\) | Correlação cruzada da entrada com o gradiente de saída | Uma convolução |
| \(\partial L / \partial X\) | Convolução "full" do gradiente de saída com o kernel rotacionado 180° | Uma convolução (a "transposta") |
A rotação não é um truque: derivar \(Y = W \star X\) em relação a \(X\) troca os papéis dos dois argumentos e trocá-los inverte a ordem dos índices. A visão im2col, que transforma tudo isso numa única multiplicação de matrizes, está no CS231n11.
A mesma conta, com números
Uma entrada 3×3, um kernel 2×2 e viés 1, com passo 1 e sem preenchimento:
Cada posição da saída é uma janela multiplicada termo a termo e somada. A primeira vale \(1{\cdot}1 + 2{\cdot}0 + 4{\cdot}(-1) + 5{\cdot}1 + 1 = 3\). As quatro juntas dão
Agora suponha que a perda devolva \(\partial L/\partial Y = \begin{bmatrix} 0{,}5 & -0{,}5 \\ 1 & 0 \end{bmatrix}\). As três derivadas da tabela, nesses números:
- Viés — soma tudo: \(0{,}5 - 0{,}5 + 1 + 0 = 1\).
- Kernel — correlaciona a entrada com o gradiente. A primeira entrada é \(0{,}5{\cdot}1 - 0{,}5{\cdot}2 + 1{\cdot}4 + 0{\cdot}5 = 3{,}5\) e a matriz inteira fica \(\begin{bmatrix} 3{,}5 & 4{,}5 \\ 6{,}5 & 7{,}5 \end{bmatrix}\).
- Entrada — convolução full do gradiente com o kernel: \(\begin{bmatrix} 0{,}5 & -0{,}5 & 0 \\ 0{,}5 & 1 & -0{,}5 \\ -1 & 1 & 0 \end{bmatrix}\).
Repare no formato: o gradiente da entrada é 3×3, do tamanho de \(X\), enquanto o que chegou era 2×2. Cada pixel participou de tantas janelas quanto o kernel o alcançou — o canto entrou em uma, o centro entrou em quatro — e a convolução full é justamente a conta que soma as contribuições de todas elas. É por isso que ela precisa de \(k-1\) de preenchimento de cada lado, não \(k-1\) no total.
"""A passagem reversa de uma convolução, com números, conferida por diferenças finitas.
As três derivadas são convoluções — é isso que a página afirma e é isso que o print mostra.
"""
import numpy as np
from scipy.signal import correlate2d, convolve2d
def forward(X, K, b):
return correlate2d(X, K, mode="valid") + b # correlação cruzada: é o que as bibliotecas chamam de conv
def backward(dY, X, K):
dX = convolve2d(dY, K, mode="full") # full: dY cresce k-1 de CADA lado
dK = correlate2d(X, dY, mode="valid")
db = dY.sum()
return dX, dK, db
X = np.array([[1., 2., 3.], [4., 5., 6.], [7., 8., 9.]])
K = np.array([[1., 0.], [-1., 1.]])
b = 1.0
dY = np.array([[0.5, -0.5], [1.0, 0.0]]) # gradiente que chega de cima
Y = forward(X, K, b)
dX, dK, db = backward(dY, X, K)
# Conferência: L = sum(dY * Y) tem gradiente exatamente dY, então dL/dX deve bater com dX.
eps, num = 1e-6, np.zeros_like(X)
for i in range(X.shape[0]):
for j in range(X.shape[1]):
Xp, Xm = X.copy(), X.copy()
Xp[i, j] += eps; Xm[i, j] -= eps
num[i, j] = ((dY * forward(Xp, K, b)).sum() - (dY * forward(Xm, K, b)).sum()) / (2 * eps)
np.set_printoptions(precision=2, suppress=True)
print("Y =\n", Y)
print("dY =\n", dY)
print("dX =\n", dX)
print("dK =\n", dK)
print("db =", db)
print("erro máximo contra diferenças finitas:", f"{np.abs(num - dX).max():.1e}")
Y =
[[3. 4.]
[6. 7.]]
dY =
[[ 0.5 -0.5]
[ 1. 0. ]]
dX =
[[ 0.5 -0.5 0. ]
[ 0.5 1. -0.5]
[-1. 1. 0. ]]
dK =
[[3.5 4.5]
[6.5 7.5]]
db = 1.0
erro máximo contra diferenças finitas: 5.1e-10
A última linha é uma conferência por diferenças finitas: sem ela, um erro de preenchimento como o descrito acima passa despercebido, porque a saída continua com um formato plausível.
Convolução transposta e o xadrez
Essa mesma operação \(\partial L / \partial X\), usada para frente, é o ConvTranspose2d — a forma padrão de fazer upsampling em decodificadores e GANs. Quando o passo não divide o tamanho do kernel, posições de saída recebem números desiguais de contribuições e o resultado é um xadrez visível7. A correção que todo mundo usa hoje: upsample e depois convolução (nn.Upsample + Conv2d), nunca uma convolução transposta pura.
Pooling: o que ele descarta e para onde o gradiente volta
Uma camada de pooling não tem um único peso. Ela resume cada janela num número e a escolha do resumo decide quem recebe gradiente:
Na volta, o max não reparte nada. A janela inteira recebe zero, menos a posição que venceu, que recebe o gradiente inteiro:
O average troca esse indicador por \(1/k^2\), que é a mesma conta com o crédito dividido igualmente. O painel mostra as duas, com um gradiente de 1 em cada posição da saída, para que o número que aparece na volta seja literalmente em quantas janelas aquela célula venceu.
Na configuração padrão — janela 2×2, passo 2 — 27 das 36 células da entrada não recebem nada. Elas não influenciaram saída nenhuma, então a perda não se move se elas mudarem um pouco. Três consequências que valem a pena carregar:
- O max pooling é um roteador, não um filtro. Ele escolhe um caminho por janela e o gradiente só desce por ele. Trocar para average faz todas as células receberem \(1/k^2\) — mais estável, menos seletivo.
- A rota muda durante o treino. O vencedor de hoje pode não ser o de amanhã, porque ele depende dos valores, não dos índices. É por isso que o max pooling não é uma operação fixa como parece.
- Com passo menor que a janela as janelas se sobrepõem e uma célula pode acumular o gradiente de várias. Arraste o passo para 1 e veja o número subir.
"""Max pooling: a ida guarda o índice do vencedor, a volta devolve o gradiente só para ele."""
import numpy as np
def forward(X, k=2, s=2):
H, W = X.shape
Y = np.zeros((H // s, W // s))
arg = np.zeros_like(Y, dtype=int) # índice achatado do vencedor de cada janela
for i in range(0, H - k + 1, s):
for j in range(0, W - k + 1, s):
win = X[i:i + k, j:j + k]
arg[i // s, j // s] = np.argmax(win) # empate: argmax escolhe o primeiro
Y[i // s, j // s] = win.flat[arg[i // s, j // s]]
return Y, arg
def backward(dY, arg, shape, k=2, s=2):
dX = np.zeros(shape)
for i in range(dY.shape[0]):
for j in range(dY.shape[1]):
di, dj = divmod(arg[i, j], k)
dX[i * s + di, j * s + dj] = dY[i, j] # o resto da janela fica em zero
return dX
X = np.arange(1., 17.).reshape(4, 4)
dY = np.array([[0.5, -0.5], [1.0, 0.0]])
Y, arg = forward(X)
dX = backward(dY, arg, X.shape)
np.set_printoptions(precision=2, suppress=True)
print("X =\n", X)
print("Y =\n", Y)
print("dY =\n", dY)
print("dX =\n", dX)
print("gradiente que chegou:", dY.sum(), "· gradiente distribuído:", dX.sum(),
"· posições que receberam algo:", int((dX != 0).sum()), "de", X.size)
X =
[[ 1. 2. 3. 4.]
[ 5. 6. 7. 8.]
[ 9. 10. 11. 12.]
[13. 14. 15. 16.]]
Y =
[[ 6. 8.]
[14. 16.]]
dY =
[[ 0.5 -0.5]
[ 1. 0. ]]
dX =
[[ 0. 0. 0. 0. ]
[ 0. 0.5 0. -0.5]
[ 0. 0. 0. 0. ]
[ 0. 1. 0. 0. ]]
gradiente que chegou: 1.0 · gradiente distribuído: 1.0 · posições que receberam algo: 3 de 16
Flatten: a ponte para a cabeça densa
Entre a última convolução e uma camada densa é preciso desmanchar o tensor \((B, C, H, W)\) em um vetor \((B,\; C \cdot H \cdot W)\). Isso é o flatten: ele não tem parâmetro, não faz conta nenhuma e a passagem reversa é só remontar o formato original. O que ele tem é um custo escondido — é ele que transforma o mapa inteiro em entrada de uma matriz densa gigantesca — era daí que vinha a maior parte dos parâmetros da AlexNet e da VGG. A substituição por global average pooling, na tabela acima, eliminou esse custo de uma vez.
Como a arquitetura ganhou o formato que tem
Quinze anos de pesquisa em visão — da LeNet-51 à AlexNet2 — deixaram um punhado de movimentos que continuam em todo modelo e uma pilha maior que não. Vale saber qual é qual.
-
O que sobreviveu
- Kernels 3×3, empilhados — o argumento do VGG, acima3.
- Blocos residuais — ResNet4; sem eles nada além de ~20 camadas treina. Hoje estão em literalmente toda arquitetura, convolucional ou não.
- Normalização (batch/group) depois de cada convolução.
- Global average pooling no lugar da cabeça totalmente conectada — a maior parte da antiga contagem de parâmetros estava naquela cabeça e ela não fazia nada que o GAP não faça melhor.
- Convoluções separáveis em profundidade — fatoram um kernel \(k{\times}k{\times}C_{in}{\times}C_{out}\) em uma parte espacial e uma parte \(1{\times}1\) de mistura de canais, por cerca de \(1/C_{out} + 1/k^2\) do custo5. Todo modelo que precisa rodar num celular usa isso.
- Convoluções 1×1 — uma camada densa por posição; como toda arquitetura muda o número de canais barato.
-
O que não
- Local Response Normalization (AlexNet) — superada pelo BatchNorm e extinta.
- Cabeças classificadoras totalmente conectadas com dezenas de milhões de parâmetros.
- Kernels grandes na entrada (11×11, 7×7 com passo 4) — substituídos por pilhas de 3×3, ou por patch embedding.
- Classificadores auxiliares no meio da rede (GoogLeNet) — um remendo para o gradiente que desaparece, que as conexões residuais resolveram direito.
- Sigmoid e tanh dentro de pilhas de visão.
- A própria corrida por backbones do ImageNet — um novo backbone de classificação projetado à mão deixou de ser uma contribuição publicável e já faz anos.
ConvNeXt: o experimento de controle
É tentador concluir de 2020–2021 que atenção venceu convolução. O ConvNeXt8 é o experimento que testa isso direito: pegue uma ResNet-50 e mude só as coisas que não são atenção — a receita de treino (AdamW, 300 épocas, augmentation pesada), as proporções entre estágios, um kernel 7×7 depthwise, LayerNorm no lugar de BatchNorm, GELU no lugar de ReLU, menos normalizações por bloco. Nenhuma atenção em lugar nenhum.
O resultado empata com o Swin Transformer em todas as escalas. A leitura honesta não é "convoluções vencem" — é que boa parte da vantagem reportada dos ViTs era a receita de treino, não a arquitetura e que as duas famílias convergiram para o mesmo desenho de bloco vindo de direções opostas. Onde as arquiteturas de fato ainda diferem é na escala de dados: veja o capítulo 13.
Onde as convoluções realmente vivem em 2026
Elas perderam a tarefa de manchete e mantiveram quase todo o resto. Isso importa para as escolhas do seu projeto:
| Ainda convolucional e não por inércia | Por quê |
|---|---|
| O VAE de todo modelo de difusão latente | Stable Diffusion, SDXL, SD3 e FLUX codificam pixels em latentes com um autoencoder convolucional. O prior está certo e o custo é linear no número de pixels — veja o capítulo 20. |
| Segmentação médica e científica | O nnU-Net9 segue como o padrão que ganha benchmarks em dezenas de tarefas biomédicas; os conjuntos de treino têm centenas de volumes, não bilhões de imagens e um prior forte vale mais que capacidade. |
| Visão em dispositivo e em tempo real | Detecção e segmentação a 30 fps num celular ou num carro. Modelos da classe MobileNet/EfficientNet, separáveis em profundidade de ponta a ponta. |
| Áudio, espectrogramas, séries temporais | Convoluções 1-D seguem sendo a baseline barata e forte. |
| A entrada de muitos modelos "Transformer" | O patch embedding de um ViT é uma convolução com passo; muitos modelos híbridos mantêm vários estágios convolucionais de verdade antes do primeiro bloco de atenção. |
| Hoje normalmente Transformer | Por quê |
|---|---|
| Classificação e busca de imagens em larga escala | Relações globais já na primeira camada e retorno melhor do pré-treino em escala web. |
| Reconhecimento de vocabulário aberto | Treino no estilo CLIP (capítulo 19) exige uma torre de texto e um espaço compartilhado. |
| Backbones de geração texto-para-imagem | DiT e MMDiT substituíram a U-Net10 no SD3 e no FLUX (capítulo 22). |
A regra que generaliza para além deste capítulo
Uma convolução é um prior forte, barato e fixo. Um Transformer tem um prior fraco, caro e aprendível. O prior forte vence quando os dados são escassos, a latência importa ou a estrutura é genuinamente local; o fraco vence quando você pode pagar por dados suficientes para aprender uma estrutura melhor do que a que você teria imposto. Esse trade-off é o eixo ao longo do qual os próximos quatro capítulos se movem.
Pontos principais
- Uma convolução é uma camada densa com localidade e compartilhamento de pesos impostos. É capacidade retirada, não acrescentada e as duas restrições são afirmações sobre imagens.
- Compartilhar pesos dá equivariância à translação. Invariância vem do pooling global e da augmentation — e nada dá invariância a rotação ou escala de graça.
- O campo receptivo é o orçamento de projeto. O passo é o que o faz crescer geometricamente; a dilatação compra alcance sem downsampling.
- Duas camadas 3×3 ganham de uma 5×5 — menos pesos, mais não linearidade. É por isso que 3×3 é o padrão.
- A passagem reversa são três convoluções; usada para frente, a do gradiente da entrada é a convolução transposta e ela produz xadrez. Prefira upsample + convolução.
- Da arquitetura clássica, sobreviveram pilhas 3×3, residuais, normalização, global average pooling, separáveis em profundidade e convoluções 1×1. As cabeças densas gigantes, a LRN e os kernels grandes de entrada, não.
- O ConvNeXt mostra que boa parte da vantagem dos ViTs era a receita de treino. As famílias convergiram.
- As convoluções perderam a corrida de backbones do ImageNet e mantiveram os autoencoders latentes, a segmentação médica, a visão em dispositivo e o patch stem — que é a maior parte dos pixels em produção no mundo.
-
LeCun, Y., Bottou, L., Bengio, Y., & Haffner, P. (1998). Gradient-Based Learning Applied to Document Recognition — Proc. IEEE. LeNet-5: convolução, pooling e retropropagação já montados no formato usado até hoje. ↩
-
Krizhevsky, A., Sutskever, I., & Hinton, G. (2012). ImageNet Classification with Deep Convolutional Neural Networks — NIPS. AlexNet; o resultado que tornou GPUs equipamento padrão. ↩
-
Simonyan, K., & Zisserman, A. (2015). Very Deep Convolutional Networks for Large-Scale Image Recognition — ICLR. VGG; o argumento "empilhe 3×3 em vez de usar um kernel grande", feito com cuidado. ↩↩
-
He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep Residual Learning for Image Recognition — CVPR. ↩
-
Howard, A., et al. (2017). MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications. Convoluções separáveis em profundidade, com a contabilidade de custo escrita por extenso. ↩
-
Yu, F., & Koltun, V. (2016). Multi-Scale Context Aggregation by Dilated Convolutions — ICLR. Campo receptivo crescendo exponencialmente a resolução constante. ↩
-
Odena, A., Dumoulin, V., & Olah, C. (2016). Deconvolution and Checkerboard Artifacts — Distill. Interativo e a razão pela qual "upsample e depois convolua" virou padrão. ↩
-
Liu, Z., Mao, H., Wu, C.-Y., Feichtenhofer, C., Darrell, T., & Xie, S. (2022). A ConvNet for the 2020s — CVPR. A ablação que separa a receita da arquitetura. ↩
-
Isensee, F., Jaeger, P., Kohl, S., Petersen, J., & Maier-Hein, K. (2021). nnU-Net: a self-configuring method for deep learning-based biomedical image segmentation — Nature Methods. Por que uma U-Net bem configurada ainda é o que se tem de bater. ↩
-
Ronneberger, O., Fischer, P., & Brox, T. (2015). U-Net: Convolutional Networks for Biomedical Image Segmentation — MICCAI. O encoder–decoder com conexões de salto que depois virou o backbone da difusão. ↩
-
Karpathy, A., et al. CS231n: Deep Learning for Computer Vision — Stanford. Ainda o melhor tratamento gratuito da mecânica, incluindo a visão
im2colda convolução como uma única multiplicação de matrizes. ↩