Pular para o conteúdo principal

Tutorial de PyTorch: Criação de uma rede neural simples a partir do zero

Conheça os conceitos básicos de PyTorch e entenda em detalhes o funcionamento das redes neurais. Comece a usar o PyTorch hoje mesmo.
Atualizado 24 de jul. de 2024  · 11 min lido

Tutorial de PyTorch Rede Neural

Neste tutorial de PyTorch, vamos abordar as principais funções que alimentam as redes neurais e criar as nossas próprias redes do zero. O principal objetivo deste artigo é demonstrar os conceitos básicos de PyTorch, uma biblioteca de tensores de aprendido profundo otimizada, ao mesmo tempo em que apresenta a você um histórico detalhado de como funcionam as redes neurais. 

Observação: confira este espaço de trabalho do DataCamp para acompanhar os códigos escritos neste artigo.

O que são redes neurais? 

As redes neurais também são chamadas de redes neurais artificiais (ANNs, Artificial Neural Networks). A arquitetura forma a base do aprendizado profundo, que é apenas um subconjunto do aprendizado automático que trata de algoritmos inspirados na estrutura e no funcionamento do cérebro humano.  Simplificando, as redes neurais formam a base de arquiteturas que imitam a forma como os neurônios biológicos transmitem sinais entre si. 

Consequentemente, você encontrará com frequência recursos que passam os primeiros cinco minutos descrevendo a estrutura neural do cérebro humano para ajudá-lo a visualizar como funciona uma rede neural. Mas quando você não tem cinco minutos sobrando, é mais fácil definir uma rede neural como uma função que associa entradas a saídas desejadas. 

A arquitetura genérica de uma rede neural consiste no seguinte:

  • Camada de entrada: Os dados são inseridos na rede por meio da camada de entrada. O número de neurônios na camada de entrada é equivalente ao número de variáveis independentes nos dados. Tecnicamente, a camada de entrada não é considerada uma das camadas da rede porque não ocorre nenhum tipo de computação nesse ponto. 
  • Camada oculta: As camadas que ficam entre as camadas de entrada e saída são chamadas de camadas ocultas. Uma rede pode ter um número arbitrário de camadas ocultas. Quanto mais camadas ocultas houver, mais complexa será a rede. 
  • Camada de saída: A camada de saída é usada para fazer uma previsão. 
  • Neurônios: Cada camada tem um conjunto de neurônios que interagem com os neurônios de outras camadas. 
  • Função de ativação: Realiza transformações não lineares para ajudar o modelo a aprender padrões complexos a partir dos dados. 

Rede neural de três camadas

Observe que a rede neural exibida na imagem acima seria considerada uma rede neural de três camadas, e não de quatro – isso porque não a camada de entrada não foi considerada. Portanto, o número de camadas de uma rede é o número de camadas ocultas mais a camada de saída.

Como funcionam as redes neurais? 

Vamos desmembrar o algoritmo em componentes menores para que você entenda melhor como funcionam as redes neurais.

Inicialização dos pesos

A inicialização dos pesos é o primeiro componente da arquitetura da rede neural. Os pesos iniciais que configuramos definem o ponto de partida do processo de otimização do modelo de rede neural. 

A forma como definimos os pesos é importante, sobretudo ao criar redes profundas. Isso ocorre porque as redes profundas são mais propensas a sofrer com o problema de gradiente explosivo ou dissipação do gradiente. Os problemas de gradiente explosivo e dissipação do gradiente são dois conceitos que vão além do escopo deste artigo, mas ambos descrevem uma situação em que o algoritmo não consegue aprender. 

Embora a inicialização dos pesos não resolva completamente o problema do gradiente explosivo ou da dissipação do gradiente, certamente contribui para sua prevenção. 

Aqui vão algumas abordagens comuns de inicialização dos pesos: 

Inicialização zero

A inicialização zero significa que os pesos são inicializados como zero. Não é uma boa solução, pois a rede neural não conseguiria quebrar a simetria – ela não aprenderia. 

Sempre que um valor constante é usado para inicializar os pesos de uma rede neural, podemos esperar que ela tenha um desempenho ruim, pois todas as camadas aprenderão a mesma coisa. Se todas as saídas das unidades ocultas tiverem a mesma influência sobre o custo, então os gradientes serão idênticos. 

Inicialização aleatória

A inicialização aleatória quebra a simetria, o que significa que é melhor do que a inicialização zero, mas alguns fatores podem ditar a qualidade geral do modelo. 

Por exemplo: se os pesos forem inicializados aleatoriamente com valores altos, podemos esperar que cada multiplicação de matrizes resulte em um valor significativamente maior. Quando uma função de ativação sigmoide é aplicada nessas situações, o resultado é um valor próximo a um, o que diminui a taxa de aprendizado.

Outra situação em que a inicialização aleatória pode causar problemas é quando os pesos são inicializados aleatoriamente com valores pequenos. Nesse caso, cada multiplicação de matrizes gera valores significativamente menores, e a aplicação de uma função sigmoide produz um valor mais próximo de zero, o que também diminui a taxa de aprendizado. 

Inicialização de Xavier/Glorot 

Uma inicialização de Xavier, também conhecida como inicialização de Glorot, é uma abordagem heurística usada para inicializar pesos. É comum ver essa abordagem de inicialização sempre que uma função de ativação tanh ou sigmoide é aplicada à média ponderada. A abordagem foi proposta pela primeira vez em 2010, em um artigo de pesquisa intitulado Understanding the difficulty of training deep feedforward neural networks (Entendendo a dificuldade de treinar redes neurais profundas feedforward), de Xavier Glorot e Yoshua Bengio. Essa técnica de inicialização visa manter a variância igual em toda a rede para evitar que os gradientes explodam ou desapareçam. 

Inicialização de He/Kaiming 

A inicialização de He ou Kaiming é outra abordagem heurística. A diferença entre as heurísticas de He e de Xavier é que a inicialização de He usa um fator de escala diferente para os pesos considerando a não linearidade das funções de ativação. 

Assim, quando a função de ativação ReLU é usada nas camadas, a inicialização de He é a abordagem recomendada. Para saber mais sobre essa abordagem, acesse . Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification, de He et al.

Melhores cursos sobre redes neurais

Propagação direta

As redes neurais funcionam calculando uma média ponderada mais um termo de viés (bias) e aplicam uma função de ativação para adicionar uma transformação não linear. Na formulação da média ponderada, cada peso determina a importância de cada variável independente ou feature (ou seja, o quanto ela contribui para prever o resultado).

Weighted average sum in a neural network.png

A fórmula acima é a média ponderada somada a um termo de viés, onde 

  • z é a soma ponderada da entrada de um neurônio
  • Wn denota os pesos
  • Xn denota as variáveis independentes e
  • b é o termo de viés.

Se a fórmula lhe parece familiar, é porque se trata de um regressão linear. Sem introduzir a não linearidade nos neurônios, teríamos uma regressão linear, que é um modelo simples. A transformação não linear permite que a rede neural aprenda padrões complexos. 

Funções de ativação

Já fizemos alusão a algumas funções de ativação na seção de inicialização de pesos, mas agora você sabe a importância delas na arquitetura de uma rede neural.

Vamos analisar melhor algumas funções de ativação comuns que você deve encontrar ao ler artigos de pesquisa e códigos de outras pessoas.  

Sigmoide

A função sigmoide é caracterizada por uma curva em forma de "S" que assume apenas valores entre zero e um. É uma função diferenciável, ou seja, é possível encontrar a inclinação da curva em dois pontos quaisquer, e monotônica, o que significa que não é nem totalmente crescente nem decrescente. Normalmente, a função de ativação sigmoide é usada em problemas de classificação binária.

Veja como você pode visualizar sua própria função sigmoide usando Python: 

# Sigmoid function in Python
import matplotlib.pyplot as plt
import numpy as np




x = np.linspace(-5, 5, 50)
z = 1/(1 + np.exp(-x))




plt.subplots(figsize=(8, 5))
plt.plot(x, z)
plt.grid()
plt.show()

Gráfico de linhas da função sigmoide

Tanh

A tangente hiperbólica (tanh) tem a mesma curva em forma de "S" que a função sigmoide, mas os valores ficam entre -1 e 1. Assim, entradas pequenas são mapeadas mais próximas de -1, e entradas maiores são mapeadas mais próximas de 1.

Expressão da função Tanh

Aqui vai um exemplo da função tanh visualizada usando Python: 

# tanh function in Python
import matplotlib.pyplot as plt
import numpy as np




x = np.linspace(-5, 5, 50)
z = np.tanh(x)


plt.subplots(figsize=(8, 5))
plt.plot(x, z)
plt.grid()
plt.show()

Gráfico de linhas da função tanh

Softmax

A função softmax costuma ser usada como função de ativação na camada de saída. É uma generalização da função sigmoide para várias dimensões. Assim, é usada em redes neurais para prever a qual classe um elemento pertence com base em mais de dois rótulos. 

Unidade linear retificada (ReLU)

O uso da função sigmoide ou tanh para criar redes neurais profundas é arriscado, pois é mais provável que sofram com o problema da dissipação do gradiente. A unidade linear retificada (ReLU, Rectified Linear Unit) é uma função de ativação criada como solução para esse problema e costuma ser a função de ativação padrão em várias redes neurais. 

Aqui vai um exemplo visual da função ReLU usando Python: 

# ReLU in Python
import matplotlib.pyplot as plt
import numpy as np


x = np.linspace(-5, 5, 50)
z = [max(0, i) for i in x]


plt.subplots(figsize=(8, 5))
plt.plot(x, z)
plt.grid()
plt.show()

Gráfico de linhas da função ReLU

A ReLU assume valores entre zero e infinito. Observe que, para valores de entrada menores ou iguais a zero, a função retorna zero e, para valores acima de zero, a função retorna o valor de entrada fornecido (ou seja, se você inserir 2, será retornado 2). A função ReLU acaba se comportando de forma extremamente semelhante a uma função linear, o que a torna muito mais fácil de otimizar e implementar. 

O processo da camada de entrada até a camada de saída é conhecido como passagem para frente ou propagação para frente. Durante essa fase, as saídas geradas pelo modelo são usadas para calcular uma função de custo a fim de identificar o desempenho da rede neural após cada iteração. Essas informações são então repassadas ao modelo para corrigir os pesos, de modo que o modelo possa fazer previsões melhores em um processo conhecido como retropropagação. 

Retropropagação

No final da primeira passagem para frente, a rede faz previsões usando os pesos inicializados, que não foram ajustados. Portanto, é muito provável que as previsões feitas pelo modelo não sejam precisas. Usando a perda calculada na propagação para frente, passamos informações de volta pela rede a fim de ajustar os pesos em um processo conhecido como retropropagação. 

Por fim, usamos a função de otimização para ajudar a identificar os pesos que podem reduzir a taxa de erro, tornando o modelo mais confiável e aumentando sua capacidade de generalização para novas instâncias. A matemática de como isso funciona está além do escopo deste artigo, mas o leitor interessado pode aprender mais sobre retropropagação em nosso curso Introdução ao Aprendizado Profundo em Python.

Tutorial de PyTorch: explicação passo a passo de como criar uma rede neural desde o início

Nesta seção do artigo, vamos criar um modelo simples de rede neural artificial usando a biblioteca PyTorch. Confira este espaço de trabalho do DataCamp para acompanhar o código

O PyTorch é uma das bibliotecas mais conhecidas para aprendizado profundo. Ele oferece uma experiência de depuração muito mais direta do que o TensorFlow. O PyTorch tem várias outras vantagens, como treinamento distribuído, um ecossistema robusto, suporte à nuvem, permitindo que você escreva códigos prontos para produção, etc. Para saber mais sobre o PyTorch, confira o programa de habilidades Introdução ao aprendizado profundo com o PyTorch

Vamos começar o tutorial. 

Definição e preparação dos dados 

O conjunto de dados que usaremos em nosso tutorial é o make_circles, do scikit-learn. Consulte a documentação. Trata-se de um conjunto de dados sobre brinquedos que contém um círculo grande com um círculo menor em um plano bidimensional e duas variáveis independentes. Em nossa demonstração, usamos 10.000 amostras e adicionamos um ruído gaussiano com desvio-padrão de 0,05 aos dados. 

Antes de criarmos a rede neural, é uma boa prática dividir os dados em conjuntos de treinamento e teste para poder avaliar o desempenho do modelo em dados não vistos.

import matplotlib.pyplot as plt
from sklearn.datasets import make_circles
from sklearn.model_selection import train_test_split

# Create a dataset with 10,000 samples.
X, y = make_circles(n_samples = 10000,
                    noise= 0.05,
                    random_state=26)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.33, random_state=26)

# Visualize the data.
fig, (train_ax, test_ax) = plt.subplots(ncols=2, sharex=True, sharey=True, figsize=(10, 5))
train_ax.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=plt.cm.Spectral)
train_ax.set_title("Training Data")
train_ax.set_xlabel("Feature #0")
train_ax.set_ylabel("Feature #1")

test_ax.scatter(X_test[:, 0], X_test[:, 1], c=y_test)
test_ax.set_xlabel("Feature #0")
test_ax.set_title("Testing data")
plt.show()

Conjuntos de dados de treinamento e teste

A próxima etapa é converter os dados de treinamento e teste de matrizes NumPy em tensores PyTorch. Para isso, vamos criar um conjunto de dados personalizado para nossos arquivos de treinamento e teste. Também vamos utilizar o módulo Dataloader do PyTorch para poder treinar os dados em lotes. Aqui está o código: 

import warnings
warnings.filterwarnings("ignore")

!pip install torch -q

import torch
import numpy as np
from torch.utils.data import Dataset, DataLoader

# Convert data to torch tensors
class Data(Dataset):
    def __init__(self, X, y):
        self.X = torch.from_numpy(X.astype(np.float32))
        self.y = torch.from_numpy(y.astype(np.float32))
        self.len = self.X.shape[0]
       
    def __getitem__(self, index):
        return self.X[index], self.y[index]
   
    def __len__(self):
        return self.len
   
batch_size = 64

# Instantiate training and test data
train_data = Data(X_train, y_train)
train_dataloader = DataLoader(dataset=train_data, batch_size=batch_size, shuffle=True)

test_data = Data(X_test, y_test)
test_dataloader = DataLoader(dataset=test_data, batch_size=batch_size, shuffle=True)

# Check it's working
for batch, (X, y) in enumerate(train_dataloader):
    print(f"Batch: {batch+1}")
    print(f"X shape: {X.shape}")
    print(f"y shape: {y.shape}")
    break


"""
Batch: 1
X shape: torch.Size([64, 2])
y shape: torch.Size([64])
"""

Agora vamos implementar e treinar a rede neural. 

Implementação da rede neural e treinamento do modelo

Vamos implementar uma rede neural simples de duas camadas que usa a função de ativação ReLU (torch.nn.functional.relu). Para isso, vamos criar uma classe chamada NeuralNetwork que herda as propriedades de nn.Module, a classe base para todos os módulos de rede neural criados no PyTorch. 

Aqui está o código: 

import torch
from torch import nn
from torch import optim

input_dim = 2
hidden_dim = 10
output_dim = 1

class NeuralNetwork(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        super(NeuralNetwork, self).__init__()
        self.layer_1 = nn.Linear(input_dim, hidden_dim)
        nn.init.kaiming_uniform_(self.layer_1.weight, nonlinearity="relu")
        self.layer_2 = nn.Linear(hidden_dim, output_dim)
       
    def forward(self, x):
        x = torch.nn.functional.relu(self.layer_1(x))
        x = torch.nn.functional.sigmoid(self.layer_2(x))

        return x
       
model = NeuralNetwork(input_dim, hidden_dim, output_dim)
print(model)


"""
NeuralNetwork(
  (layer_1): Linear(in_features=2, out_features=10, bias=True)
  (layer_2): Linear(in_features=10, out_features=1, bias=True)
)
"""

Isso é tudo. 

Para treinar o modelo, precisamos definir uma função de perda a ser usada para calcular os gradientes e um otimizador para atualizar os parâmetros. Em nossa demonstração, usaremos a entropia cruzada binária e um gradiente descendente estocástico com taxa de aprendizado de 0,1. 

learning_rate = 0.1

loss_fn = nn.BCELoss()

optimizer = torch.optim.SGD(model.parameters(), lr=lea

Vamos treinar o modelo

num_epochs = 100
loss_values = []


for epoch in range(num_epochs):
    for X, y in train_dataloader:
        # zero the parameter gradients
        optimizer.zero_grad()
       
        # forward + backward + optimize
        pred = model(X)
        loss = loss_fn(pred, y.unsqueeze(-1))
        loss_values.append(loss.item())
        loss.backward()
        optimizer.step()

print("Training Complete")

"""
Training Complete
"""

Como monitoramos os valores de perda, podemos visualizar a perda do modelo ao longo do tempo. 

step = np.linspace(0, 100, 10500)

fig, ax = plt.subplots(figsize=(8,5))
plt.plot(step, np.array(loss_values))
plt.title("Step-wise Loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.show()

Treinamento do modelo

A visualização acima mostra a perda do modelo em 100 épocas. Inicialmente, a perda começa em 0,7 e diminui gradualmente. Isso indica que o modelo está melhorando as previsões ao longo do tempo. No entanto, o modelo parece atingir um patamar em torno da marca de 60 épocas, o que pode ter uma série de motivos, como o fato de o modelo estar na região de um mínimo local ou global da função de perda. 

Contudo, o modelo foi treinado e está pronto para fazer previsões sobre novas instâncias – vamos ver como fazer isso na próxima seção.

Previsões e avaliação do modelo

Fazer previsões com nossa rede neural PyTorch é bem simples. 

"""
We're not training so we don't need to calculate the gradients for our outputs
"""
with torch.no_grad():
    for X, y in test_dataloader:
        outputs = model(X)
        predicted = np.where(outputs < 0.5, 0, 1)
        predicted = list(itertools.chain(*predicted))
        y_pred.append(predicted)
        y_test.append(y)
        total += y.size(0)
        correct += (predicted == y.numpy()).sum().item()

print(f'Accuracy of the network on the 3300 test instances: {100 * correct // total}%')

"""
Accuracy of the network on the 3300 test instances: 97%
"""

Observação: cada execução do código gera uma saída diferente, portanto talvez você não obtenha os mesmos resultados. 

O código acima faz um loop pelos lotes de teste, que são armazenados na variável test_dataloader, sem calcular os gradientes. Em seguida, prevemos as instâncias no lote e armazenamos os resultados em uma variável chamada outputs. Depois disso, definimos todos os valores menores que 0,5 como 0 e aqueles iguais ou maiores que 0,5 como 1. Em seguida, esses valores são anexados a uma lista para nossas previsões.

Depois disso, adicionamos as previsões reais das instâncias no lote a uma variável chamada total. Na sequência, calculamos o número de previsões corretas identificando o número de previsões iguais às classes reais e totalizando-as. O número total de previsões corretas para cada lote é incrementado e armazenado em nossa variável correct.

Para calcular a precisão do modelo como um todo, multiplicamos o número de previsões corretas por 100 (para obter uma porcentagem) e, em seguida, dividimos pelo número de instâncias em nosso conjunto de teste. Nosso modelo teve 97% de precisão. Continuamos a análise usando a matriz de confusão e o classification_report do scikit-learn para entender melhor o desempenho do modelo.

from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix

import seaborn as sns


y_pred = list(itertools.chain(*y_pred))
y_test = list(itertools.chain(*y_test))


print(classification_report(y_test, y_pred))

"""
              precision    recall  f1-score   support

        0.0       0.98      0.97      0.98      1635
        1.0       0.98      0.98      0.98      1665

    accuracy                           0.98      3300
  macro avg       0.98      0.98      0.98      3300
weighted avg       0.98      0.98      0.98      3300

"""




cf_matrix = confusion_matrix(y_test, y_pred)

plt.subplots(figsize=(8, 5))

sns.heatmap(cf_matrix, annot=True, cbar=False, fmt="g")

plt.show()

Previsões da rede neural

Nosso modelo está funcionando muito bem. Incentivo você a analisar o código e fazer algumas alterações para assimilar o que foi abordado neste artigo.  

Neste tutorial do PyTorch, abordamos os fundamentos básicos das redes neurais e usamos o PyTorch, uma biblioteca Python para aprendizado profundo, na implementação de nossa rede. Usamos o conjunto de dados do circles do scikit-learn para treinar uma rede neural de duas camadas para classificação. Em seguida, fizemos previsões sobre os dados e avaliamos os resultados usando a métrica de precisão. 

Tópicos

Cursos de Python

Curso

Python intermediário

4 h
1.3M
Aumente o nível de suas habilidades em ciência de dados criando visualizações usando Matplotlib e manipulando DataFrames com pandas.
Ver detalhesRight Arrow
Iniciar curso
Ver maisRight Arrow
Relacionado

Tutorial

Criação de modelos de redes neurais (NN) em R

Neste tutorial, você aprenderá a criar um modelo de rede neural no R.
Abid Ali Awan's photo

Abid Ali Awan

Tutorial

Guia de torchchat do PyTorch: Configuração local com Python

Saiba como configurar o torchchat do PyTorch localmente com Python neste tutorial prático, que fornece orientação e exemplos passo a passo.
GNN

Tutorial

Uma introdução abrangente às redes neurais de grafos (GNNs)

Saiba tudo sobre Graph Neural Networks, inclusive o que são GNNs, os diferentes tipos de redes neurais de grafos e para que são usadas. Além disso, saiba como criar uma Graph Neural Network com o Pytorch.
Abid Ali Awan's photo

Abid Ali Awan

Tutorial

Introdução às redes neurais profundas

Compreensão das redes neurais profundas e sua importância no mundo moderno da aprendizagem profunda da inteligência artificial
Bharath K's photo

Bharath K

Tutorial

Uma introdução ao Q-Learning: Um tutorial para iniciantes

Saiba mais sobre o algoritmo mais popular de aprendizado por reforço sem modelo com um tutorial em Python.
Abid Ali Awan's photo

Abid Ali Awan

Ver maisVer mais