Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Redes Neurais Convolucionais

Open In Colab

Redes Neurais Convolucionais

As redes neurais convolucionais (CNNs) surgiram como uma evolução natural das redes neurais perceptron multicamadas (MLPs) para lidar com dados estruturados em forma de grade, como imagens. Enquanto as MLPs conectam todos os neurônios de uma camada à seguinte, as CNNs exploram propriedades espaciais, aplicando filtros locais (convoluções) que capturam padrões como bordas, texturas e formas. Isso reduz drasticamente o número de parâmetros em comparação com MLPs para entradas de alta dimensão, além de introduzir indutivas de localidade e invariância translacional, tornando-as mais adequadas para visão computacional.

A arquitetura básica de uma CNN combina três blocos principais: camadas convolucionais (responsáveis por extrair características locais), camadas de pooling (que reduzem a dimensionalidade e aumentam a robustez a pequenas variações) e camadas totalmente conectadas no final (responsáveis pela decisão ou regressão, semelhante às MLPs). Esse desenho hierárquico permite que a rede aprenda representações de baixo nível até conceitos de alto nível. Assim, CNNs superaram MLPs em tarefas como reconhecimento de imagens, tornando-se o paradigma dominante em visão computacional até o surgimento de arquiteturas baseadas em attention, como Transformers.

Limitações da MLP

A MLP é uma arquitetura versátil que, de acordo com o teorema de aproximação universal (Hornik et al, 1989), pode aprender qualquer função. Apesar disso, podem ser necessários muitos neurônios e, consequentemente, muitos parâmetros. Neste caso, em tarefas muito complexas e com muitos parâmetros, torna-se necessário ter à disposição muitos dados (Alwosheel et al, 2018), o que geralmente não é possível.

Em tarefas como visão computacional ou análise de sinais, podemos ter centenas a milhares de atributos de entrada (pixels/amostras) que, quando conectadas com camadas ocultas com centenas a milhares de neurônios, geram milhares a milhões de parâmetros que devem ser aprendidos. Nestes casos, seriam necessárias ainda mais amostras (em torno de 10 a 50 vezes o número de parâmetros).

Apesar disso, além de termos alta dimensionalidade em tais problemas, existe também uma estrutura espacial ou temporal que a MLP não explora de forma eficiente, pois trata cada entrada como independente. Nesse contexto, surgem as redes neurais convolucionais (CNNs), que utilizam filtros locais aplicados sobre regiões da entrada para extrair características relevantes, reduzindo o número de parâmetros e explorando a correlação entre vizinhos. Essa abordagem permite capturar bordas, texturas e padrões repetitivos de maneira hierárquica, tornando as CNNs especialmente adequadas para problemas de visão computacional e análise de sinais.

Camada de Convolução

A operação de convolução resolve esse problema ao substituir a conexão densa da MLP por kernels (ou filtros) de tamanho reduzido, que percorrem a imagem aplicando o mesmo conjunto de pesos em diferentes regiões. Em vez de aprender um peso para cada pixel em relação a cada neurônio, o kernel aprende um pequeno conjunto de parâmetros compartilhados, capaz de detectar padrões locais como bordas horizontais, verticais ou texturas. Esse mecanismo reduz drasticamente o número de parâmetros e garante que uma mesma característica possa ser reconhecida em qualquer posição da imagem.

Visualmente, o kernel pode ser entendido como um “perceptron local” que analisa apenas uma vizinhança da entrada, e cuja aplicação repetida sobre toda a imagem permite compor representações cada vez mais complexas.

A convolução introduz dois conceitos fundamentais: compartilhamento de pesos e invariância translacional. Como o mesmo kernel é aplicado em toda a extensão da imagem, a rede não precisa aprender separadamente o mesmo padrão em diferentes posições, o que reduz ainda mais a quantidade de parâmetros e aumenta a capacidade de generalização. Além disso, uma característica detectada em um ponto (como uma borda ou textura) poderá ser reconhecida em qualquer outra região da imagem. Dessa forma, a convolução estabelece a base para a extração de features robustas e reutilizáveis, preparando o terreno para a sequência natural de operações em CNNs: funções de ativação, pooling e, por fim, camadas densas para a decisão final.

Funções de Ativação

Após a convolução, aplica-se uma função de ativação não linear para permitir que a rede aprenda representações complexas. Nas CNNs, popularizou-se o uso da ReLU e suas variantes, como Leaky ReLU e GeLU.

Camada de Pooling

A camada de pooling é utilizada para reduzir a dimensionalidade espacial das representações sem perder informações essenciais. Ela aplica uma operação de agregação em regiões locais da saída da convolução, sendo as mais comuns o max pooling (que seleciona o valor máximo) e o average pooling (que calcula a média). Esse processo traz dois benefícios principais: diminui o número de parâmetros e cálculos nas camadas seguintes e aumenta a robustez da rede a pequenas variações na entrada, já que detalhes locais são resumidos em características mais estáveis.

Camadas de Normalização

Camadas de normalização surgiram para estabilizar e acelerar o treinamento de redes profundas. Nas primeiras CNNs, era comum o uso de Local Response Normalization (LRN), que normalizava a ativação de um neurônio considerando seus vizinhos próximos, inspirada em mecanismos biológicos de inibição lateral. Mais tarde, a técnica de Batch Normalization (BatchNorm) se tornou dominante, pois normaliza as ativações de cada camada em um mini-batch, reduzindo o internal covariate shift e permitindo taxas de aprendizado maiores e convergência mais estável.

Camadas Densas (ou Completamente Conectadas)

Por fim, após as etapas de convolução, ativação, pooling e normalização, as CNNs geralmente utilizam camadas densas para realizar a decisão final. Essas camadas funcionam de forma idêntica às que já vimos na MLP: todos os neurônios são conectados a todas as entradas, permitindo combinar as características extraídas anteriormente em uma representação de alto nível. Assim, enquanto as camadas convolucionais atuam como extratoras de features, as camadas densas cumprem o papel de classificador ou regressor, dependendo da tarefa.

Decisões de Projeto de Redes Neurais Convolucionais

Em redes convolucionais, as escolhas arquiteturais determinam a capacidade do modelo de extrair padrões em diferentes escalas e níveis de abstração. A profundidade (número de camadas) aumenta a hierarquia de representações, enquanto a largura (número de filtros por camada) define a diversidade de características extraídas. O tamanho do kernel controla o campo receptivo local — kernels maiores capturam padrões mais amplos, enquanto menores focam em detalhes finos. O stride (passo) ajusta o quanto o filtro se desloca sobre a entrada, influenciando a redução de dimensionalidade e a sobreposição das regiões analisadas. Outras decisões, como padding ou uso de camadas de pooling, também afetam a resolução espacial e o equilíbrio entre custo computacional e riqueza das representações.

Profundidade e Largura

Em redes convolucionais, profundidade refere-se ao número de camadas sucessivas (convolucionais ou blocos) e está ligada à capacidade do modelo de aprender representações progressivamente mais abstratas: redes mais profundas capturam padrões de alto nível a partir de combinações de características mais simples. Já a largura diz respeito ao número de filtros (ou canais de saída) em cada camada, controlando quanta variedade de padrões diferentes pode ser extraída em um mesmo nível de processamento. O aumento de profundidade tende a melhorar a hierarquia das representações, enquanto o aumento de largura amplia a diversidade de características aprendidas — ambos impactando diretamente a capacidade do modelo, custo computacional e risco de sobreajuste (overfitting).

Parâmetros da Convolução: Tamanho de Kernel, Stride e Padding

O tamanho do kernel em uma camada convolucional determina a área local da entrada que cada filtro observa ao extrair características. Kernels pequenos (como 3×3) capturam padrões locais finos e são comuns em arquiteturas modernas por permitirem composições profundas com menos parâmetros, enquanto kernels maiores (como 5×5 ou 7×7) já cobrem regiões mais amplas e capturam padrões de contexto mais extenso em uma única operação. Esse tamanho, junto com stride, padding e o número de camadas, define o campo receptivo efetivo do modelo — ou seja, a região da entrada que influencia cada neurônio nas camadas posteriores. Ajustar o tamanho do kernel e o campo receptivo é essencial para equilibrar resolução espacial, custo computacional e capacidade de capturar dependências de longo alcance.

O passo (ou stride) controla de quantos pixels o filtro se desloca sobre a entrada a cada operação de convolução. Passos menores (como 1) mantêm maior sobreposição entre janelas consecutivas, preservando mais detalhes espaciais, mas aumentam o custo computacional. Passos maiores (como 2) reduzem a dimensionalidade mais rapidamente, atuando de forma semelhante a um downsampling inicial, porém com perda de resolução espacial. Ajustar o stride permite equilibrar granularidade das características extraídas e eficiência da rede.

O padding define como lidar com as bordas da entrada antes da convolução, adicionando valores artificiais (geralmente zeros) ao redor do mapa de entrada. O padding “válido” (sem adicionar bordas) reduz gradualmente as dimensões, enquanto o padding “mesmo” (same padding) preserva as dimensões originais da entrada após a convolução. Essa escolha afeta tanto o campo receptivo quanto a capacidade do modelo de processar informações nas bordas, influenciando diretamente o tamanho dos mapas de ativação e a profundidade possível da rede sem encolher demais a representação.

Kernels Especiais: Dilatação, Assimetria e Convolução 1x1

A dilatação (dilation ou atrous convolution) controla o espaçamento entre os elementos do kernel, inserindo “buracos” entre os pesos do filtro. Isso aumenta o campo receptivo da convolução sem aumentar o número de parâmetros ou perder resolução espacial por downsampling. Com dilatações maiores, a rede consegue capturar dependências de longo alcance em uma única camada, enquanto dilatações menores se comportam como convoluções tradicionais. Essa técnica é muito usada em tarefas que exigem preservação de resolução e contexto amplo, como segmentação semântica ou detecção de padrões em sinais.

Além das convoluções de múltiplas escalas, um avanço significativo que surgiu em versões posteriores, como na Inception v2 e Inception v3 (Szegedy et al, 2014), foi o uso de convoluções assimétricas, como as convoluções 3x1 e 1x3. Essas convoluções assimétricas foram introduzidas para melhorar ainda mais a eficiência da rede. Ao decompor uma convolução regular de 3x3 em duas operações menores, como uma convolução 3x1 (captura de características na dimensão horizontal) e uma 1x3 (captura de características na dimensão vertical), a rede pode manter o mesmo campo receptivo de uma convolução 3x3, porém com menos parâmetros e menor custo computacional. Essa técnica ajuda a rede a capturar padrões em direções específicas de forma mais eficiente, além de reduzir a quantidade de cálculos envolvidos.

As convoluções 1x1 foram introduzidas para melhorar a eficiência computacional e a flexibilidade das redes neurais convolucionais, especialmente em arquiteturas como a Inception. Embora inicialmente possam parecer triviais, as convoluções 1x1 têm um papel crucial em reduzir a dimensionalidade dos dados sem perder informações relevantes. Ao aplicar um filtro 1x1, a convolução é realizada apenas sobre a profundidade (ou canais) da entrada, sem afetar a largura ou altura da imagem. Isso permite a fusão de informações de diferentes canais, o que facilita a combinação de características complexas de forma eficiente. Além disso, essa técnica reduz o número de parâmetros e operações, resultando em uma rede mais leve e rápida, o que foi fundamental para o sucesso de redes profundas como a GoogLeNet/Inception (Szegedy et al, 2014) e ResNet (He et al, 2016).

Conexões Residuais

As conexões residuais foram a principal inovação da ResNet (He et al, 2016) e representam um avanço crucial no design de redes neurais profundas. Elas introduzem um atalho que permite que o sinal original da entrada seja passado diretamente para uma camada posterior, sem passar pelas camadas intermediárias, criando o que é chamado de mapeamento de identidade. Esse conceito resolve o problema do gradiente desvanecente, que dificultava o treinamento de redes muito profundas, ao garantir que os gradientes possam fluir de forma mais eficaz para as camadas anteriores. As conexões residuais permitem que a rede aprenda diferenças residuais em vez de tentar ajustar o mapeamento completo da função, o que facilita o aprendizado. Como resultado, redes muito profundas, como as versões da ResNet com 50, 101 ou até 152 camadas, podem ser treinadas de maneira eficaz, sem degradação de desempenho, permitindo que elas capturem representações complexas dos dados.

Aplicações de Redes Neurais Convolucionais

Embora as CNNs sejam amplamente conhecidas por suas aplicações em visão computacional, elas também têm sido adaptadas para lidar com séries temporais e outros tipos de dados não visuais. Em séries temporais, as CNNs podem capturar padrões locais ao longo do tempo, assim como capturam padrões espaciais em imagens, aplicando convoluções para detectar tendências, sazonalidades ou eventos relevantes em sequências temporais. Por exemplo, CNNs são usadas em previsões financeiras, onde conseguem identificar padrões em dados históricos de preços, ou em análise de sinais biomédicos, como ECGs, para detectar anomalias de saúde. Além disso, CNNs têm sido aplicadas a dados textuais para tarefas de classificação de texto e análise de sentimentos, onde as convoluções identificam sequências de palavras ou frases relevantes. Ao lidar com dados sequenciais, as CNNs competem com RNNs e Transformers, mostrando que são versáteis e eficazes em diversos tipos de dados, além de imagens.

Exemplo

No seguinte exemplo, vamos desenvolver uma rede MLP simples e uma LetNet-5 para o famoso problema MNIST, de reconhecimento de dígitos.

import seaborn as sns

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms

from torch.utils.data import DataLoader
from tqdm import tqdm
from sklearn.metrics import f1_score, confusion_matrix
# Transformações dos dados
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])

# Carregar o dataset MNIST
trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
trainloader = DataLoader(trainset, batch_size=64, shuffle=True)

testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
testloader = DataLoader(testset, batch_size=1000, shuffle=False)
100%|██████████| 9.91M/9.91M [00:00<00:00, 133MB/s]
100%|██████████| 28.9k/28.9k [00:00<00:00, 29.5MB/s]
100%|██████████| 1.65M/1.65M [00:00<00:00, 106MB/s]
100%|██████████| 4.54k/4.54k [00:00<00:00, 6.63MB/s]
i = 101
plt.imshow(trainset[i][0][0])
plt.title(f'Amostra {i} - Classe {trainset[i][1]}')
plt.show()
<Figure size 640x480 with 1 Axes>
class MLP(nn.Module):
    def __init__(self):
        super(MLP, self).__init__()
        self.fc1 = nn.Linear(28 * 28, 128)  # 28px X 28px
        self.fc2 = nn.Linear(128, 10)  # Saída de 10 classes para o MNIST

    def forward(self, x):
        x = x.view(-1, 28*28)  # Flatten
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return x
# Instanciar a rede, a função de perda e o otimizador
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = MLP().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# Treinamento
epochs = 5
epoch_losses = []

for epoch in range(epochs):
    running_loss = 0.0
    progress_bar = tqdm(trainloader, desc=f"Epoch {epoch+1}/{epochs}", leave=False)

    for inputs, labels in progress_bar:
        inputs, labels = inputs.to(device), labels.to(device)

        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        progress_bar.set_postfix(loss=loss.item())

    avg_loss = running_loss / len(trainloader)
    epoch_losses.append(avg_loss)

# Plot training loss per epoch
plt.figure()
plt.plot(range(1, epochs+1), epoch_losses, marker='o')
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Training Loss per Epoch")
plt.grid(True)
plt.show()
Fetching long content....
<Figure size 640x480 with 1 Axes>
# Avaliação no conjunto de teste
model.eval()
y_true = []
y_pred = []

with torch.no_grad():
    for inputs, labels in testloader:
        inputs, labels = inputs.to(device), labels.to(device)
        outputs = model(inputs)
        _, predicted = torch.max(outputs.data, 1)

        y_true.extend(labels.cpu().numpy())
        y_pred.extend(predicted.cpu().numpy())

# Accuracy
accuracy = (np.array(y_true) == np.array(y_pred)).mean() * 100
print(f'Accuracy on test set: {accuracy:.2f}%')

# F1-macro score only
f1_macro = f1_score(y_true, y_pred, average='macro')
print(f'F1 Macro: {f1_macro:.4f}')

# Confusion Matrix
cm = confusion_matrix(y_true, y_pred)
plt.figure(figsize=(8,6))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues")
plt.xlabel("Predicted")
plt.ylabel("True")
plt.title("Confusion Matrix")
plt.show()
Accuracy on test set: 96.01%
F1 Macro: 0.9600
<Figure size 800x600 with 2 Axes>
class LeNet5(nn.Module):
    def __init__(self):
        super(LeNet5, self).__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5)  # MNIST tem 1 canal de entrada
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
        self.fc1 = nn.Linear(16 * 4 * 4, 120)  # As dimensões após o conv2 dependem do tamanho da entrada
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)  # Saída de 10 classes para o MNIST

    def forward(self, x):
        x = torch.relu(self.conv1(x))
        x = torch.max_pool2d(x, kernel_size=2, stride=2)
        x = torch.relu(self.conv2(x))
        x = torch.max_pool2d(x, kernel_size=2, stride=2)
        x = x.view(-1, 16 * 4 * 4)  # Flatten
        x = torch.relu(self.fc1(x))
        x = torch.relu(self.fc2(x))
        x = self.fc3(x)
        return x
# Instanciar a rede, a função de perda e o otimizador
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = LeNet5().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# Treinamento
epochs = 5
epoch_losses = []

for epoch in range(epochs):
    running_loss = 0.0
    progress_bar = tqdm(trainloader, desc=f"Epoch {epoch+1}/{epochs}", leave=False)

    for inputs, labels in progress_bar:
        inputs, labels = inputs.to(device), labels.to(device)

        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        progress_bar.set_postfix(loss=loss.item())

    avg_loss = running_loss / len(trainloader)
    epoch_losses.append(avg_loss)

# Plot training loss per epoch
plt.figure()
plt.plot(range(1, epochs+1), epoch_losses, marker='o')
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Training Loss per Epoch")
plt.grid(True)
plt.show()
Fetching long content....
<Figure size 640x480 with 1 Axes>
# Avaliação no conjunto de teste
model.eval()
y_true = []
y_pred = []

with torch.no_grad():
    for inputs, labels in testloader:
        inputs, labels = inputs.to(device), labels.to(device)
        outputs = model(inputs)
        _, predicted = torch.max(outputs.data, 1)

        y_true.extend(labels.cpu().numpy())
        y_pred.extend(predicted.cpu().numpy())

# Accuracy
accuracy = (np.array(y_true) == np.array(y_pred)).mean() * 100
print(f'Accuracy on test set: {accuracy:.2f}%')

# F1-macro score only
f1_macro = f1_score(y_true, y_pred, average='macro')
print(f'F1 Macro: {f1_macro:.4f}')

# Confusion Matrix
cm = confusion_matrix(y_true, y_pred)
plt.figure(figsize=(8,6))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues")
plt.xlabel("Predicted")
plt.ylabel("True")
plt.title("Confusion Matrix")
plt.show()
Accuracy on test set: 98.71%
F1 Macro: 0.9870
<Figure size 800x600 with 2 Axes>

Considerações Finais

Neste capítulo, falamos sobre redes neurais convolucionais e seus principais componentes.

Próximos capítulos

Nos próximos capítulos, falaremos sobre arquiteturas especializadas em modelagem de sequencias, como séries temporais e texto, utilizando redes neurais recorrentes e transformers.

Exercícios

  1. Dobre a largura da LeNet-5.

  2. Dobre a profundidade da LeNet-5.

  3. Apresente o resultado das novas arquiteturas e discuta sobre (número de parâmetros, profundidade e acurácia).

Referências

  • Hornik, K., Stinchcombe, M., & White, H. (1989). Multilayer feedforward networks are universal approximators. Neural networks, 2(5), 359-366.

  • Alwosheel, A., Van Cranenburgh, S., & Chorus, C. G. (2018). Is your dataset big enough? Sample size requirements when using artificial neural networks for discrete choice analysis. Journal of choice modelling, 28, 167-182.

  • LeCun, Y., Bottou, L., Bengio, Y., & Haffner, P. (1998). Gradient-based learning applied to document recognition. Proceedings of the IEEE, 86(11), 2278-2324.

  • Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). Imagenet classification with deep convolutional neural networks. Advances in neural information processing systems, 25.

  • Nair, V., & Hinton, G. E. (2010). Rectified linear units improve restricted boltzmann machines. In Proceedings of the 27th international conference on machine learning (ICML-10) (pp. 807-814).

  • Szegedy, C., Liu, W., Jia, Y., Sermanet, P., Reed, S., Anguelov, D., ... & Rabinovich, A. (2015). Going deeper with convolutions. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 1-9).

  • He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep residual learning for image recognition. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 770-778).