Pesquisa de site

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 RGB
  • nn.MaxPool2d é uma camada de pooling máximo que requer apenas o tamanho do kernel e o avanço
  • nn.Linear é a camada totalmente conectada e nn.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.

Artigos relacionados: