Pular para o conteúdo principal
InicioTutoriaisPython

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.
Actualizado jul. de 2024  · 16 min leer

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. 

Temas

Cursos de Python

Course

Introduction to Deep Learning with PyTorch

4 hr
23.8K
Learn how to build your first neural network, adjust hyperparameters, and tackle classification and regression problems in PyTorch.
See DetailsRight Arrow
Start Course
Ver maisRight Arrow
Relacionado

tutorial

Criando um transformador com o PyTorch

Saiba como criar um modelo Transformer usando o PyTorch, uma ferramenta avançada de aprendizado de máquina moderno.
Arjun Sarkar's photo

Arjun Sarkar

26 min

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

16 min

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.
François Aubry's photo

François Aubry

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

15 min

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

13 min

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

16 min

See MoreSee More