Escrevendo CNNs do zero em PyTorch
Introdução
Neste artigo, construiremos Redes Neurais Convolucionais (CNNs) do zero no PyTorch e as veremos em ação enquanto as treinamos e testamos em um conjunto de dados do mundo real.
Começaremos explorando o que são CNNs e como funcionam. Em seguida, examinaremos o PyTorch e começaremos carregando o conjunto de dados CIFAR10 usando torchvision
(uma biblioteca contendo vários conjuntos de dados e funções auxiliares relacionadas à visão computacional). Em seguida, construiremos e treinaremos nossa CNN do zero. Finalmente, testaremos nosso modelo.
Abaixo está o esboço do artigo:
- Introdução
- Redes Neurais Convolucionais
- PyTorch
- Carregamento de dados
- CNN do zero
- Configurando hiperparâmetros
- Treinamento
- Teste
Pré-requisitos
É necessário um conhecimento básico de código Python e redes neurais para acompanhar este tutorial, juntamente com uma familiaridade com a estrutura PyTorch. Recomendamos este artigo para codificadores intermediários a avançados com experiência em desenvolvimento usando PyTorch.
O código neste artigo pode ser executado em um PC doméstico normal ou em um DigitalOcean Droplet e não requer VRAM significativo.
Redes Neurais Convolucionais
Uma rede neural convolucional (CNN) pega uma imagem de entrada e a classifica em qualquer uma das classes de saída. Cada imagem passa por uma série de camadas diferentes – principalmente camadas convolucionais, camadas de pooling e camadas totalmente conectadas. A imagem abaixo resume o que uma imagem passa em uma CNN:
Fonte: trabalhos de matemática
Camada Convolucional
A camada convolucional é usada para extrair recursos da imagem de entrada. É uma operação matemática entre a imagem de entrada e o kernel (filtro). O filtro é passado pela imagem e a saída é calculada da seguinte forma:
Fonte: https://www.ibm.com/cloud/learn/convolutional-neural-networks
Diferentes filtros são usados para extrair diferentes tipos de recursos. Alguns recursos comuns são fornecidos abaixo:
Fonte: https://en.wikipedia.org/wiki/Kernel_(image_processing)
Agrupando camadas
Camadas de pooling são usadas para reduzir o tamanho de qualquer imagem, mantendo os recursos mais importantes. Os tipos mais comuns de camadas de pooling usadas são pooling máximo e médio, que obtêm o valor máximo e médio, respectivamente, do tamanho determinado do filtro (ou seja, 2x2, 3x3 e assim por diante).
O pool máximo, por exemplo, funcionaria da seguinte forma:
Fonte: https://cs231n.github.io/convolutional-networks/
PyTorch
PyTorch é uma das bibliotecas de aprendizagem profunda mais populares e amplamente utilizadas – especialmente em pesquisas acadêmicas. É uma estrutura de aprendizado de máquina de código aberto que acelera o caminho desde a prototipagem de pesquisa até a implantação de produção e a usaremos hoje neste artigo para criar nossa primeira CNN.
Carregamento de dados: o conjunto de dados
Vamos começar carregando alguns dados. Estaremos usando o conjunto de dados CIFAR-10. O conjunto de dados possui 60.000 imagens coloridas (RGB) em 32px x 32px pertencentes a 10 classes diferentes (6.000 imagens/classe). O conjunto de dados é dividido em 50.000 imagens de treinamento e 10.000 imagens de teste.
Você pode ver uma amostra do conjunto de dados junto com suas classes abaixo:
Fonte: https://www.cs.toronto.edu/~kriz/cifar.html
Importando as Bibliotecas
Vamos começar importando as bibliotecas necessárias e definindo algumas variáveis:
# Load in relevant libraries, and alias where appropriate
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
Define relevant variables for the ML task
batch_size = 64
num_classes = 10
learning_rate = 0.001
num_epochs = 20
Device will determine whether to run the training on GPU or CPU.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
Importando Bibliotecas
device
determinará se o treinamento será executado em GPU ou CPU.
Carregamento de conjunto de dados
Para carregar o conjunto de dados, usaremos os conjuntos de dados integrados em torchvision
. Ele nos fornece a capacidade de baixar o conjunto de dados e também aplicar quaisquer transformações que desejarmos.
Vejamos o código primeiro:
# Use transforms.compose method to reformat images for modeling,and save to variable all_transforms for later use
all_transforms = transforms.Compose([transforms.Resize((32,32)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.4914, 0.4822, 0.4465],
std=[0.2023, 0.1994, 0.2010])
])Create Training dataset
train_dataset = torchvision.datasets.CIFAR10(root = './data',
train = True,
transform = all_transforms,
download = True)
Create Testing dataset
test_dataset = torchvision.datasets.CIFAR10(root = './data',
train = False,
transform = all_transforms,
download=True)
Instantiate loader objects to facilitate processing
train_loader = torch.utils.data.DataLoader(dataset = train_dataset,
batch_size = batch_size,
shuffle = True)
test_loader = torch.utils.data.DataLoader(dataset = test_dataset,
batch_size = batch_size,
shuffle = True)
Carregando e transformando dados
Vamos dissecar este trecho de código:
- Começamos escrevendo algumas transformações. Redimensionamos as imagens, convertemos em tensores e normalizamos usando a média e o desvio padrão de cada banda nas imagens de entrada. Você também pode calculá-los, mas eles estão disponíveis online.
- Em seguida, carregamos o conjunto de dados: treinamento e teste. Definimos download igual a True para que seja baixado, caso ainda não tenha sido baixado.
- Carregar todo o conjunto de dados na RAM de uma só vez não é uma boa prática e pode travar seriamente o seu computador. É por isso que usamos carregadores de dados, que permitem iterar pelo conjunto de dados carregando os dados em lotes.
- Em seguida, criamos dois carregadores de dados (para treinamento/teste) e definimos o tamanho do lote, junto com o embaralhamento, igual a True, para que as imagens de cada classe sejam incluídas em um lote.
CNN do zero
Antes de mergulhar no código, vamos explicar como definir uma rede neural no PyTorch.
- Você começa criando uma nova classe que estende a classe
nn.Module
do PyTorch. Isso é necessário quando estamos criando uma rede neural, pois nos fornece vários métodos úteis - Temos então que definir as camadas da nossa rede neural. Isso é feito no método
__init__
da classe. Simplesmente nomeamos nossas camadas e depois as atribuímos à camada apropriada que desejamos; por exemplo, camada convolucional, camada de pooling, camada totalmente conectada, etc. - A última coisa a fazer é definir um método
forward
em nossa classe. O objetivo deste método é definir a ordem em que os dados de entrada passam pelas diversas camadas.
Agora, vamos mergulhar no código:
# Creating a CNN class
class ConvNeuralNet(nn.Module): Determine what layers and their order in CNN object
def __init__(self, num_classes):
super(ConvNeuralNet, self).__init__()
self.conv_layer1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3)
self.conv_layer2 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=3)
self.max_pool1 = nn.MaxPool2d(kernel_size = 2, stride = 2)
self.conv_layer3 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3)
self.conv_layer4 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3)
self.max_pool2 = nn.MaxPool2d(kernel_size = 2, stride = 2)
self.fc1 = nn.Linear(1600, 128)
self.relu1 = nn.ReLU()
self.fc2 = nn.Linear(128, num_classes)
# Progresses data across layers
def forward(self, x):
out = self.conv_layer1(x)
out = self.conv_layer2(out)
out = self.max_pool1(out)
out = self.conv_layer3(out)
out = self.conv_layer4(out)
out = self.max_pool2(out)
out = out.reshape(out.size(0), -1)
out = self.fc1(out)
out = self.relu1(out)
out = self.fc2(out)
return out
Como expliquei acima, começamos criando uma classe que herda a classe nn.Module
, e depois definimos as camadas e sua sequência de execução dentro de __init__
e encaminhar
respectivamente.
Algumas coisas a serem observadas aqui:
nn.Conv2d
é usado para definir as camadas convolucionais. Definimos os canais que eles recebem e quanto devem retornar junto com o tamanho do kernel. Partimos de 3 canais, pois estamos usando imagens RGBnn.MaxPool2d
é uma camada de pooling máximo que requer apenas o tamanho do kernel e o avançonn.Linear
é a camada totalmente conectada enn.ReLU
é a função de ativação usada- No método
forward
, definimos a sequência e, antes das camadas totalmente conectadas, remodelamos a saída para corresponder à entrada a uma camada totalmente conectada
Configurando hiperparâmetros
Vamos agora definir alguns hiperparâmetros para fins de treinamento.
model = ConvNeuralNet(num_classes)
Set Loss function with criterion
criterion = nn.CrossEntropyLoss()
Set optimizer with optimizer
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay = 0.005, momentum = 0.9)
total_step = len(train_loader)
Hiperparâmetros
Começamos inicializando nosso modelo com o número de classes. Em seguida, escolhemos entropia cruzada e SGD (Stochastic Gradient Descent) como nossa função de perda e otimizador, respectivamente. Existem diferentes opções para eles, mas descobri que resultam em precisão máxima ao experimentar. Também definimos a variável total_step
para facilitar a iteração através de vários lotes.
Treinamento
Agora, vamos começar a treinar nosso modelo:
# We use the pre-defined number of epochs to determine how many iterations to train the network on
for epoch in range(num_epochs):Load in the data in batches using the train_loader object
for i, (images, labels) in enumerate(train_loader):
# Move tensors to the configured device
images = images.to(device)
labels = labels.to(device)
# Forward pass
outputs = model(images)
loss = criterion(outputs, labels)
# Backward and optimize
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('Epoch [{}/{}], Loss: {:.4f}'.format(epoch+1, num_epochs, loss.item()))
Esta é provavelmente a parte mais complicada do código. Vamos ver o que o código faz:
- Começamos iterando o número de épocas e, em seguida, os lotes em nossos dados de treinamento
- Convertemos as imagens e os rótulos de acordo com o dispositivo que estamos usando, ou seja, GPU ou CPU
- No forward pass, fazemos previsões usando nosso modelo e calculamos as perdas com base nessas previsões e em nossos rótulos reais.
- Em seguida, fazemos o retrocesso onde realmente atualizamos nossos pesos para melhorar nosso modelo
- Em seguida, definimos os gradientes como zero antes de cada atualização usando a função
optimizer.zero_grad()
- Em seguida, calculamos os novos gradientes usando a função
loss.backward()
- E finalmente, atualizamos os pesos com a função
optimizer.step()
Podemos ver a saída da seguinte forma:
Perdas de treinamento
Como podemos ver, a perda diminui ligeiramente com o aumento do número de épocas. Este é um bom sinal. Mas você pode notar que ele está flutuando no final, o que pode significar que o modelo está superajustado ou que o batch_size
é pequeno. Teremos que testar para descobrir o que está acontecendo.
Teste
Vamos agora testar nosso modelo. O código para teste não é tão diferente do treinamento, com exceção do cálculo dos gradientes, pois não estamos atualizando nenhum peso:
with torch.no_grad():
correct = 0
total = 0
for images, labels in train_loader:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the {} train images: {} %'.format(50000, 100 * correct / total))
Colocamos o código dentro de torch.no_grad()
pois não há necessidade de calcular nenhum gradiente. Em seguida, prevemos cada lote usando nosso modelo e calculamos quantos ele prevê corretamente. Obtemos o resultado final com precisão de aproximadamente 83%:
Precisão
E é isso. Conseguimos criar uma Rede Neural Convolucional do zero no PyTorch!
Conclusão
Começamos aprendendo sobre as CNNs – que tipo de camadas elas possuem e como funcionam. Em seguida, apresentamos o PyTorch, que é uma das bibliotecas de aprendizado profundo mais populares disponíveis atualmente. Aprendemos como o PyTorch tornaria muito mais fácil fazer experiências com uma CNN.
Em seguida, carregamos o conjunto de dados CIFAR-10 (um conjunto de dados de treinamento popular contendo 60.000 imagens) e fizemos algumas transformações nele.
Então, construímos uma CNN do zero e definimos alguns hiperparâmetros para ela. Por fim, treinamos e testamos nosso modelo no CIFAR10 e conseguimos uma precisão decente no conjunto de teste.