Machine Learning: Redes Neurais Convolucionais
Última atualização: 2026-08-26
CNNs dão aos computadores "olhos" — os kernels de convolução agem como receptores visuais, reconhecendo imagens camada por camada, de bordas a objetos.
1. O que você vai aprender
- Operações de convolução: kernels/filtros, stride, padding, convolução multicanal
- Camadas de pooling: MaxPool/AvgPool, redução espacial e invariância à translação
- Arquiteturas clássicas: a evolução de LeNet → AlexNet → VGG → ResNet e conexões residuais
- Transfer learning: modelos pré-treinados (ImageNet) + ajuste fino para alta acurácia com dados limitados
- Classificação de imagens de produtos do SalesPredict: reconhecimento automático de categorias de produtos
2. A história real de um e-commerce
(1) O problema: categorização manual de produtos é lenta e inconsistente
A plataforma do Bob lista 1 mil novos produtos todos os dias, e a equipe de operações deve classificar manualmente as imagens dos produtos nas categorias corretas. A classificação manual é lenta (5 segundos por imagem), inconsistente (pessoas diferentes atribuem o mesmo produto a categorias diferentes) e cara ($30 mil USD por mês em custos de mão de obra). A classificação de imagens é o gargalo nas operações de e-commerce.
(2) A solução com CNN
CNNs aprendem automaticamente features visuais a partir de imagens de produtos para realizar a classificação — velocidade de milissegundos, forte consistência e zero custo de mão de obra.
import torchvision.models as models
# Transfer learning: usar ResNet pré-treinado
model = models.resnet18(pretrained=True)
model.fc = nn.Linear(model.fc.in_features, num_classes) # Substituir a última camada
(3) O resultado: 95% de acurácia na classificação, $30K economizados por mês
Bob usou CNN + transfer learning para automatizar a classificação de produtos, alcançando mais de 95% de acurácia. A velocidade de processamento melhorou de 5 segundos por imagem para 0,01 segundo por imagem, economizando $30 mil USD em custos mensais de mão de obra.
3. Como a convolução funciona
(1) Kernels e extração de features
graph TB
INPUT[Imagem de Entrada<br/>H x W x C] --> CONV1[Camada Conv 1<br/>Bordas e Linhas]
CONV1 --> POOL1[Camada Pool 1<br/>Reduz Espacial]
POOL1 --> CONV2[Camada Conv 2<br/>Texturas e Partes]
CONV2 --> POOL2[Camada Pool 2]
POOL2 --> CONV3[Camada Conv 3<br/>Objetos e Formas]
CONV3 --> FLATTEN[Flatten]
FLATTEN --> FC[Totalmente Conectada]
FC --> OUTPUT[Probabilidades das Classes]
▶ Exemplo: Implementando convolução manualmente
import torch
import torch.nn as nn
# Entrada: 1 imagem, 1 canal, 5x5
input_img = torch.tensor([[
[1, 2, 0, 1, 3],
[0, 1, 2, 3, 1],
[1, 3, 1, 0, 2],
[2, 0, 3, 1, 1],
[0, 1, 2, 0, 1],
]], dtype=torch.float32).unsqueeze(0) # shape: (1, 1, 5, 5)
# Conv2d: 1 canal de entrada, 1 canal de saída, kernel 3x3
conv = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, stride=1, padding=0)
print(f"Pesos do kernel:\n{conv.weight.data.squeeze()}")
print(f"Bias: {conv.bias.data.item():.4f}")
output = conv(input_img)
print(f"\nShape da entrada: {input_img.shape}")
print(f"Shape da saída: {output.shape}")
print(f"Saída:\n{output.squeeze().detach()}")
Saída:
# Execução bem-sucedida
(2) Parâmetros de convolução
| Parâmetro | Significado | Efeito |
|---|---|---|
| kernel_size | Tamanho do kernel | 3x3 (comum) / 5x5 / 7x7 |
| stride | Tamanho do passo | 1 (padrão) / 2 (downsampling) |
| padding | Preenchimento com zeros | 0 (encolhe) / 1 (preserva o tamanho) |
| in_channels | Número de canais de entrada | RGB=3, escala de cinza=1 |
| out_channels | Número de canais de saída | Igual ao número de kernels |
▶ Exemplo: Comparando diferentes configurações de convolução
import torch.nn as nn
x = torch.randn(1, 3, 32, 32) # 1 batch, 3 canais, 32x32
configs = {
"3x3, stride=1, pad=1": nn.Conv2d(3, 16, 3, stride=1, padding=1),
"3x3, stride=2, pad=1": nn.Conv2d(3, 16, 3, stride=2, padding=1),
"5x5, stride=1, pad=2": nn.Conv2d(3, 16, 5, stride=1, padding=2),
"5x5, stride=2, pad=0": nn.Conv2d(3, 16, 5, stride=2, padding=0),
}
for name, layer in configs.items():
out = layer(x)
print(f"{name:25s}: {x.shape} → {out.shape}")
Saída:
# Execução bem-sucedida
4. Camadas de Pooling e arquiteturas CNN
(1) Operações de pooling
▶ Exemplo: MaxPool vs. AvgPool
import torch
import torch.nn as nn
x = torch.tensor([[
[1, 3, 2, 4],
[5, 6, 7, 8],
[9, 2, 1, 3],
[4, 5, 6, 7],
]], dtype=torch.float32).unsqueeze(0) # (1, 1, 4, 4)
maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
avgpool = nn.AvgPool2d(kernel_size=2, stride=2)
print(f"Entrada:\n{x.squeeze()}")
print(f"\nMaxPool (2x2):\n{maxpool(x).squeeze()}")
print(f"\nAvgPool (2x2):\n{avgpool(x).squeeze()}")
Saída:
# Execução bem-sucedida
(2) Evolução das arquiteturas clássicas
| Arquitetura | Ano | Inovação | Profundidade | Parâmetros |
|---|---|---|---|---|
| LeNet | 1998 | Primeira CNN | 5 | 60K |
| AlexNet | 2012 | ReLU + Dropout + GPU | 8 | 60M |
| VGG | 2014 | Kernels pequenos empilhados (3x3) | 16-19 | 138M |
| ResNet | 2015 | Conexões residuais (skip connections) | 18-152 | 11-60M |
▶ Exemplo: Construindo uma CNN simples com PyTorch
import torch
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self, num_classes=5):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, 3, padding=1), # 3→32 canais, 32x32
nn.ReLU(),
nn.MaxPool2d(2, 2), # 32→16x16
nn.Conv2d(32, 64, 3, padding=1), # 32→64 canais, 16x16
nn.ReLU(),
nn.MaxPool2d(2, 2), # 16→8x8
nn.Conv2d(64, 128, 3, padding=1), # 64→128 canais, 8x8
nn.ReLU(),
nn.MaxPool2d(2, 2), # 8→4x4
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(128 * 4 * 4, 256),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(256, num_classes),
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x
model = SimpleCNN(num_classes=5)
print(model)
print(f"Parâmetros: {sum(p.numel() for p in model.parameters()):,}")
Saída:
# Função definida com sucesso
5. Transfer Learning
(1) Por que o transfer learning funciona
Modelos pré-treinados no ImageNet já aprenderam features visuais gerais (bordas → texturas → partes de objetos). Você só precisa substituir a última camada para adaptar a uma nova tarefa.
▶ Exemplo: Transfer learning com ResNet18
import torch
import torch.nn as nn
import torchvision.models as models
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import numpy as np
# Carregar ResNet18 pré-treinado
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# Congelar todas as camadas (modo extrator de features)
for param in model.parameters():
param.requires_grad = False
# Substituir a última camada totalmente conectada
num_classes = 5 # Eletrônicos, Roupas, Alimentos, Livros, Casa
model.fc = nn.Linear(model.fc.in_features, num_classes)
# Apenas a nova camada fc tem parâmetros treináveis
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"Treináveis: {trainable:,} / Total: {total:,} ({trainable/total*100:.1f}%)")
# Transformações de dados para o modelo pré-treinado
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
Saída:
# Execução bem-sucedida
(2) Estratégias de ajuste fino
| Estratégia | Camadas congeladas | Tamanho dos dados de treino | Tempo de treino | Melhor para |
|---|---|---|---|---|
| Extrator de features | Todas (treinar só fc) | Pequeno (<1k) | Rápido | Dados muito limitados |
| Ajuste fino parcial | Últimas camadas | Médio (1-10k) | Médio | Dados moderados |
| Ajuste fino completo | Nenhuma | Grande (>10k) | Lento | Dados abundantes |
▶ Exemplo: Ajuste fino das últimas camadas
import torchvision.models as models
import torch.nn as nn
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# Congelar camadas iniciais, descongelar layer4
for name, param in model.named_parameters():
if "layer4" in name or "fc" in name:
param.requires_grad = True
else:
param.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, 5)
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Parâmetros treináveis para ajuste fino: {trainable:,}")
Saída:
# Execução bem-sucedida
6. Classificação de Imagens de Produtos do SalesPredict
▶ Exemplo: Pipeline completo de treinamento com transfer learning
import torch
import torch.nn as nn
import torchvision.models as models
from torch.utils.data import DataLoader, TensorDataset
from sklearn.model_selection import train_test_split
import numpy as np
# Simular dados de imagem (na prática, carregue imagens reais com ImageFolder)
rng = np.random.default_rng(42)
n = 2000
X = rng.standard_normal((n, 3, 224, 224)).astype(np.float32) # 3 canais 224x224
y = rng.integers(0, 5, n) # 5 categorias de produtos
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
train_ds = TensorDataset(torch.FloatTensor(X_train), torch.LongTensor(y_train))
test_ds = TensorDataset(torch.FloatTensor(X_test), torch.LongTensor(y_test))
train_loader = DataLoader(train_ds, batch_size=32, shuffle=True)
test_loader = DataLoader(test_ds, batch_size=64)
# ResNet18 pré-treinado com cabeça personalizada
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
for param in model.parameters():
param.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, 5)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)
# Treinamento
for epoch in range(10):
model.train()
correct, total = 0, 0
for X_batch, y_batch in train_loader:
y_pred = model(X_batch)
loss = criterion(y_pred, y_batch)
optimizer.zero_grad()
loss.backward()
optimizer.step()
correct += (y_pred.argmax(1) == y_batch).sum().item()
total += y_batch.size(0)
if epoch % 2 == 0:
print(f"Época {epoch}: perda={loss.item():.4f}, acurácia={correct/total:.3f}")
# Avaliar
model.eval()
correct, total = 0, 0
with torch.no_grad():
for X_batch, y_batch in test_loader:
y_pred = model(X_batch)
correct += (y_pred.argmax(1) == y_batch).sum().item()
total += y_batch.size(0)
print(f"\nAcurácia no teste: {correct/total:.3f}")
Saída:
# Execução bem-sucedida
❓ Perguntas Frequentes
P: Por que as CNNs são mais adequadas para imagens do que os MLPs? R: Três razões — 1) Compartilhamento de parâmetros (o mesmo kernel varre toda a imagem, reduzindo parâmetros); 2) Conectividade local (cada neurônio só vê uma região local); 3) Invariância à translação (objetos são reconhecidos independentemente da posição). MLPs achatam imagens e perdem a estrutura espacial.
P: Quantos dados o transfer learning exige? R: O modo Extrator de Features (treinando apenas a camada fc) precisa de apenas algumas centenas de imagens; o ajuste fino das últimas camadas exige 1-10 mil imagens; treinar do zero precisa de 10 mil+ imagens. Quanto menos dados você tiver, mais camadas deve congelar.
P: Por que o kernel 3x3 é o mais usado? R: Duas convoluções 3x3 empilhadas têm o mesmo campo receptivo que um kernel 5x5, mas com menos parâmetros (18 vs. 25) e mais não linearidade (2 ativações ReLU). O VGG demonstrou que empilhar kernels pequenos supera kernels grandes.
P: O que são conexões residuais no ResNet? R: y = F(x) + x — a rede aprende o resíduo (incremento) em vez do mapeamento completo. Benefícios: resolve o problema do gradiente desaparecendo em redes profundas e permite o treinamento de redes com 100+ camadas.
P: Por que precisamos do Normalize no pré-processamento de imagens? R: Modelos pré-treinados foram normalizados usando a média e o desvio padrão do ImageNet. Novos dados devem usar a mesma normalização; caso contrário, as distribuições das features não vão coincidir e o desempenho do transfer learning será prejudicado.
P: Como lidar com desbalanceamento de classes? R: Três abordagens — 1) Função de perda ponderada (nn.CrossEntropyLoss(weight=class_weights)); 2) Oversampling da classe minoritária; 3) Data augmentation (flips/rotações/cortes aleatórios).
📖 Resumo
- Kernels de convolução agem como receptores visuais, extraindo features camada por camada: bordas → texturas → partes → objetos
- Camadas de pooling reduzem as dimensões espaciais, aumentam a invariância à translação e diminuem o custo computacional
- Arquiteturas CNN evoluíram de LeNet para ResNet; conexões residuais resolvem o problema de treinamento de redes profundas
- Transfer learning: congelar camadas pré-treinadas + substituir a cabeça de classificação para alta acurácia com dados limitados
- Três estratégias de classificação de produtos: Extrator de Features (poucos dados), Ajuste Fino Parcial (dados moderados), Ajuste Fino Completo (dados abundantes)
- O pré-processamento de imagens deve corresponder ao modelo pré-treinado (Resize + Normalize)
📝 Exercícios
- Básico (Dificuldade ⭐): Construa uma CNN de 2 camadas usando nn.Conv2d + nn.MaxPool2d com entrada 3x32x32 e saída 16x8x8. Imprima o shape após cada camada. Dica: Conv2d(3,8,3,pad=1) → Pool → Conv2d(8,16,3,pad=1) → Pool.
- Intermediário (Dificuldade ⭐⭐): Use transfer learning (ResNet18) para classificar CIFAR-10 (10 classes). Congele todas as camadas exceto fc e treine por 5 épocas. Dica: torchvision.datasets.CIFAR10 + transforms.
- Desafio (Dificuldade ⭐⭐⭐): Compare Extrator de Features vs. Ajuste Fino Parcial vs. Ajuste Fino Completo no CIFAR-10 em termos de acurácia e tempo de treinamento. Plote curvas de acurácia vs. época para todas as três estratégias. Dica: Congele diferentes números de camadas e registre a acurácia de teste a cada época.
← Lição Anterior: Introdução ao Aprendizado Profundo | Próxima Lição: Avaliação e Ajuste de Modelos →