import numpy
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torchvision import datasets, transforms
Exemplo CNN com PyTorch
Segue a implementação de uma rede CNN para resolver o problema de classificação de imagens de dígitos numéricos manuscritos do banco de dados MNIST.
Iniciando com a importação da bibliotecas:
Ajustando o valor dos hiperparâmetros:
# Ajuste de hiperparâmetros
# passo de adaptação
= 0.01
eta
# Tamanho do mini-batch
= 64
Nb
# Tamanho do mini-batch usado no teste
= 1000
Nb_test
# Número de épocas
= 1 Ne
O PyTorch tem algumas rotinas para carregar bancos de dados clássicos. Vamos usar essas rotinas para carregar o MNIST. Com o código abaixo, os dados vão ser obtidos da internet e gravados no local indicado por dir_data
, em uma pasta chamada data
.
Além disso, vamos criar dois objetos DataLoader
, para treinamento e teste, que vão se encarregar de ler os dados em partes e misturá-los:
= "~/temp"
dir_data
= torch.utils.data.DataLoader(
train_loader
datasets.MNIST(
dir_data,=True,
train=True,
download=transforms.Compose(
transform
[transforms.ToTensor()]
),
),=Nb,
batch_size=True,
shuffle
)
= torch.utils.data.DataLoader(
test_loader
datasets.MNIST(
dir_data,=False,
train=transforms.Compose(
transform
[transforms.ToTensor()]
),
),=Nb_test,
batch_size=True,
shuffle )
Vale notar alguns detalhes sobre o código anterior:
- o
DataLoader
de treinamento é criado comtrain=True
e o de teste, comtrain=False
, o que garante que não haja dados em comum entre os dois conjuntos; - É feita a configuração de uma transformação de dados ao carregá-los. Para isso, é criado um objeto do tipo
transforms.Compose
, que permite encadear uma série de transformações a serem aplicadas às imagens, durante o carregamento. Nesse caso, a transformação tem uma única etapa que consistem em converter os valores obtidos para um tensor do PyTorch.
Podemos mostrar algumas imagens do dataset usando o DataLoader que criamos:
=(16, 6))
plt.figure(figsizefor i in range(10):
2, 5, i + 1)
plt.subplot(= train_loader.dataset.__getitem__(i)
image, _
plt.imshow(image.squeeze().numpy())'off'); plt.axis(
O modelo é definido por meio de uma classe que herda de nn.Module
:
class Model(nn.Module):
def __init__(self):
# Necessário chamar __init__() da classe mãe
super().__init__()
# Camada convolucional, seguida de ReLU e Pooling
# Entra uma imagem 28x28. Com filtro 5x5, padding de 2
# e stride 1, a saída também tem 28x28. Após o pooling,
# a saída fica com 14x14. A entrada tem 1 canal e a
# saída tem 16.
self.conv1 = nn.Sequential(
nn.Conv2d(=1,
in_channels=16,
out_channels=5,
kernel_size=1,
stride=2,
padding
),
nn.ReLU(), =2),
nn.MaxPool2d(kernel_size
)
# Camada convolucional
# Entrada 14x14, saída 7x7 após o pooling.
# 16 canais de entrada e 32 de saída.
self.conv2 = nn.Sequential(
16, 32, 5, 1, 2),
nn.Conv2d(
nn.ReLU(), 2),
nn.MaxPool2d(
)
# Camada totalmente conectada
# Na entrada, há 32 canais de 7x7 elementos
# e a saída tem 10 neurônios.
self.out = nn.Linear(32 * 7 * 7, 10)
def forward(self, x):
# Aplica primeira camada convolucional
= self.conv1(x)
x
# Aplica segunda camada convolucional
= self.conv2(x)
x
# Transforma os tensores 32x7x7 em
# vetores para serem usados na entrada da
# camada totalmente conectada. Vale notar
# que a primeira dimensão dos tensores de
# dados é usada para representar os diversos
# elementos de um batch, por isso permanece
# inalterada.
= x.view(x.size(0), -1)
x
# Calcula a saída e retorna
= self.out(x)
output return output
Instanciando o modelo e definindo a função custo e o otimizador:
= torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
device
# Instanciando modelo
= Model().to(device)
model
# Função custo para treinamento
= nn.CrossEntropyLoss()
loss_function
# Otimizador
= torch.optim.Adam(model.parameters(), lr = eta) optimizer
Vale notar que a função custo CrossEntropyLoss
espera comparar um vetor de \(C\) posições com um número de \(0\) a \(C-1\), conforme descrito na documentação. Além disso, é esperado que os elementos do vetor representem a evidência, ou seja os valores chamados de logits, que não são normalizados e podem valer de \(-\infty\) a \(\infty\). Por isso, na saída da rede, não é usada a função Softmax.
Definindo o loop de treinamento:
# Lista usada para guardar o valor da função custo ao longo das iterações
= []
losses
# Loop das épocas
for epoch in range(Ne):
# Loop dos mini batches
for n, (X, d) in enumerate(train_loader):
# Envia os dados para a GPU, caso ela exista
= X.to(device=device)
X = d.to(device=device)
d
# Ajuste de dimensões
# (elementos do mini batch x 1 canal x 28 x 28)
= X.view(-1, 1, 28, 28)
X
# Coloca o modelo em modo treinamento
model.train()
# Zera informações de gradientes
model.zero_grad()
# Calcula a saída
= model(X)
y
# Calcula o valor da função custo
= loss_function(y, d)
loss
# Calcula os gradientes
loss.backward()
# Atualiza os pesos do modelo
optimizer.step()
# Armazena o valor da função custo
losses.append(loss.item())
# Mostra o valor da função custo a cada 100 iterações
if n % 100 == 0:
= len(train_loader.dataset)
N_all = n * len(X)
n_ex = 100. * n / len(train_loader)
p print(
f"Época: {epoch} [{n_ex}/{N_all} ({p:.0f}%)]\tLoss: {loss:.6f}"
)
plt.figure()
plt.plot(losses)"Batch")
plt.xlabel("Loss") plt.ylabel(
Época: 0 [0/60000 (0%)] Loss: 2.305868
Época: 0 [6400/60000 (11%)] Loss: 2.312832
Época: 0 [12800/60000 (21%)] Loss: 0.152859
Época: 0 [19200/60000 (32%)] Loss: 0.138126
Época: 0 [25600/60000 (43%)] Loss: 0.188300
Época: 0 [32000/60000 (53%)] Loss: 0.123948
Época: 0 [38400/60000 (64%)] Loss: 0.048474
Época: 0 [44800/60000 (75%)] Loss: 0.195575
Época: 0 [51200/60000 (85%)] Loss: 0.096904
Época: 0 [57600/60000 (96%)] Loss: 0.043440
Text(0, 0.5, 'Loss')
Avaliando modelo com os dados de teste:
# Variável usada para contabilizar o número de acertos
= 0
correct
# Loop dos mini batches
for n, (X, d) in enumerate(test_loader):
# Envia os dados para a GPU, caso ela exista
= X.to(device=device)
X = d.to(device=device)
d
# Ajuste de dimensões
= X.view(-1, 1, 28, 28)
X
# Coloca o modelo em modo de inferência
eval()
model.
# Calcula a saída
= model(X)
y
# Cálculo do número de acertos:
# 1) Obtém o índice do elemento máximo para cada exemplo do minibatch
= torch.max(y, 1, keepdim=True)[1]
pred # 2) Conta o número de acertos e acumula na variável correct
# pred.eq(d.view_as(pred)) é um tensor booleano. Dessa forma, o número de
# acertos é obtido somando seus elementos. Valores True são tratados como 1.
+= pred.eq(d.view_as(pred)).cpu().sum().item()
correct
# Mostra o desempenho obtido no teste
= 100. * correct / len(test_loader.dataset)
accuracy print(f"Acurácia: {correct}/{len(test_loader.dataset)} ({accuracy:.0f}%)")
Acurácia: 9690/10000 (97%)