Introdução ao PyTorch

Redes neurais simples

Autor

Enzo Shiraishi

Data de Publicação

8 de janeiro de 2025

Nesse tutorial, os principais conceitos da biblioteca PyTorch são apresentados, comparando a biblioteca com outras bibliotecas familiares, mostrando o diferencial da biblioteca e mostrando um exemplo de como criar uma rede neural simples usando o framework.

Tensores

torch é primeiramente uma biblioteca de processamento de tensores (vetores com \(n\) dimensões).

A biblioteca permite realizar diversas interações com tensores através do objeto torch.tensor usando uma interface similar ao numpy, como:

Criar tensores

import torch

tensor = torch.tensor(
    [
        [1.0, 2.0],
        [3.0, 4.0],
        [5.0, 6.0],
    ]
)
tensor.shape
torch.Size([3, 2])
tensor.dtype
torch.float32

Acessar elementos

A = torch.tensor(
    [
        [1.0, 2.0],
        [3.0, 4.0],
    ]
)

A[0]
tensor([1., 2.])
A[0, 0]
tensor(1.)
A[:, -1]
tensor([2., 4.])

Manipular tensores

A.reshape(2 * 2)
tensor([1., 2., 3., 4.])
A.view((4, 1))
tensor([[1.],
        [2.],
        [3.],
        [4.]])
# Transposição tensorial
A.T
tensor([[1., 3.],
        [2., 4.]])

Realizar operações

# x^2 realizado elemento-a-elemento em A
A**2
tensor([[ 1.,  4.],
        [ 9., 16.]])
B = torch.tensor(
    [
        [5.0, 6.0],
        [7.0, 8.0],
    ]
)

# Multiplicação elemento-a-elemento ou A.mul(B)
A * B
tensor([[ 5., 12.],
        [21., 32.]])
# Multiplicação tensorial ou A.matmul(B)
A @ B
tensor([[19., 22.],
        [43., 50.]])
# Também realiza o produto escalar em tensores unidimensionais (vetores),
# assim como A[0].dot(B[0])
A[0] @ A[1]
tensor(11.)
# Operações podem ser feitas in-place, reduzindo seu gasto de memória
B.pow_(2)
B
tensor([[25., 36.],
        [49., 64.]])

Diferenciais

Porém, o torch possui outras funcionalidades voltadas à otimização e inferência de modelos de forma computacionalmente eficiente, como:

  • Paralelismo usando GPUs
  • Diferenciação automática
  • Carregamento eficiente de dados em memória
  • Muitas utilidades para o treinamento e avaliação de redes neurais

Paralelismo usando GPUs

Além de operações em CPU, é possível usar diferentes tipos de dispositivos como GPUs para acelerar a inferêncis de certas operações através do hardware especializado.

# Caso sua máquina possua uma GPU NVIDIA
device = "cuda" if torch.cuda.is_available() else "cpu"
device
'cuda'
torch.tensor([1, 2, 3], device=device)
tensor([1, 2, 3], device='cuda:0')
tensor = torch.tensor([1, 2, 3], device="cpu")
tensor = tensor.to(device)
tensor.device
device(type='cpu')

Diferenciação automática

O PyTorch possui um motor de diferenciação automática (torch.autograd), que simplifica e otimiza o processo de otimização de parâmetros através quando técnicas como gradiente descendente e backpropagation são usadas.

O módulo torch.nn oferece a classe abstrata Module para utilizar a diferenciação automática de forma simples em componentes como:

  • Parâmetros treináveis
  • Camadas densas e funções de ativação em redes neurais
  • Funções de custo / perda / recompensa

Basta que seja possível implementar os métodos forward e backward para esse componente.

Internamente, quando objetos diferenciáveis (como Module) são criados, é gerado um grafo representando suas etapas de execução (com base nos seus atributos diferenciáveis).

from torch import nn


class NeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.input_layer = nn.Linear(5, 10)
        self.activation = nn.ReLU()
        self.output_layer = nn.Linear(10, 2)

    def forward(self, x):
        hidden_input = self.input_layer(x)
        logits_input = self.activation(hidden_input)
        logits = self.output_layer(logits_input)
        return logits


model = NeuralNetwork()

Ao executar forward, é atualizado o gradiente (atributo grad) com base na sua função (atributo grad_fn) e na topologia do grafo usando backpropagation.

loss_fn = nn.CrossEntropyLoss()
X = torch.ones(5)
y_eval = torch.ones(2)

# executa o forward implicitamente
y_pred = model(X)
loss = loss_fn(y_pred, y_eval)
loss
tensor(1.4235, grad_fn=<DivBackward1>)

Ao executar backward, é gerado um gradiente com base nos resultados do forward usando backpropagation.

loss.backward()

model.input_layer.weight.grad
tensor([[ 0.1061,  0.1061,  0.1061,  0.1061,  0.1061],
        [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
        [ 0.0294,  0.0294,  0.0294,  0.0294,  0.0294],
        [ 0.1186,  0.1186,  0.1186,  0.1186,  0.1186],
        [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
        [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
        [ 0.0493,  0.0493,  0.0493,  0.0493,  0.0493],
        [-0.0381, -0.0381, -0.0381, -0.0381, -0.0381],
        [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000],
        [ 0.0979,  0.0979,  0.0979,  0.0979,  0.0979]])

Usando esses dois métodos, é possível gerar um método de otimização para treinar os parâmetros de um modelo para um conjunto de dados de forma simples.

model = NeuralNetwork()
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters())

# Cria tensores 3D com valores aleatórios entre 0 e 1 (representando os dados de treino)
X_train_dataset = torch.randint(0, 2, (10, 5), dtype=torch.float32)
y_train_dataset = torch.randint(0, 2, (10, 2), dtype=torch.float32)

# Boa prática antes do treino para garantir o funcionamento de certos componentes
model.train()

num_epochs = 1

for epoch in range(1, num_epochs + 1):
    print(f"Epoch: {epoch}")
    for index, (X, y_eval) in enumerate(zip(X_train_dataset, y_train_dataset), 1):
        y_pred = model(X)
        loss = loss_fn(y_pred, y_eval)
        print(f"{index:>2}/{len(X_train_dataset)}: {loss:<+5.2f}")
        loss.backward()
        # Atualiza os parâmetros do modelo
        optimizer.step()
        # Zera os gradientes depois de atualizar os parâmetros
        optimizer.zero_grad()
Epoch: 1
 1/10: -0.00
 2/10: +0.71
 3/10: +0.48
 4/10: +1.39
 5/10: +0.64
 6/10: +0.86
 7/10: -0.00
 8/10: +1.39
 9/10: -0.00
10/10: -0.00