Redes Neurais - Classificação
- #Python
- #Inteligência Artificial (IA)
Neurônios biológicos são células especializadas do sistema nervoso que transmitem sinais elétricos e químicos. Eles recebem sinais de outros neurônios através de dendritos, integram esses sinais na soma do neurônio e, se o sinal integrado atingir um limiar, disparam um impulso elétrico ao longo do axônio, que é transmitido para outros neurônios nas sinapses através de neurotransmissores.
Neurônios artificiais são unidades básicas de processamento (chamadas tambem de perceptron) em redes neurais artificiais, inspiradas na estrutura e função dos neurônios biológicos. Eles recebem entradas e realizam uma combinação linear dessas entradas com pesos associados. Uma função de ativação é então aplicada ao resultado para introduzir não-linearidades na saída do neurônio. Assim como os neurônios biológicos, os neurônios artificiais contribuem para a transmissão e processamento de informações em redes neurais, facilitando a aprendizagem e a tomada de decisões em tarefas complexas.
Entradas:
- Cada neurônio recebe uma ou mais entradas (x₁, x₂, ..., xₙ).
- Essas entradas representam características ou atributos dos dados que estão sendo processados.
- Cada entrada é multiplicada por um peso correspondente antes de ser combinada.
Pesos:
- Cada entrada é associada a um peso (w₁, w₂, ..., wₙ).
- Os pesos representam a importância relativa de cada entrada para a saída do neurônio.
- Durante o treinamento da rede neural, os pesos são ajustados iterativamente para minimizar a função de perda e melhorar o desempenho da rede.
Soma ponderada:
- As entradas são multiplicadas pelos pesos correspondentes e somadas.
- Essa operação é conhecida como combinação linear e é representada pela fórmula:
z = Σᵢ=₁ⁿ wᵢ · xᵢ + b
- Onde z é a soma ponderada das entradas, wᵢ são os pesos, xᵢ são as entradas e b é o termo de viés (bias), que é adicionado para permitir que o neurônio aprenda uma função não linear.
Função de ativação:
- Após a soma ponderada, uma função de ativação f é aplicada ao resultado z.
- Essa função introduz não linearidades na saída do neurônio e permite que a rede neural modele relações complexas nos dados.
- Alguns exemplos de funções de ativação comuns são a sigmoide, ReLU (Rectified Linear Unit), tangente hiperbólica, entre outras.
Saída:
- A saída do neurônio, denotada por a, é o resultado da aplicação da função de ativação à soma ponderada das entradas:
a = f(z)
Parece complexo mas não é, suponha que temos um neurônio em uma rede neural que recebe duas entradas, x1 e x2, e queremos determinar se esse neurônio deve ser ativado com base nessas entradas. Vamos considerar a função de ativação degrau, que retorna 1 se a entrada for maior ou igual a zero, e retorna 0 caso contrário.
Além disso, o neurônio tem dois pesos associados às entradas: w1 e w2. Esses pesos determinam a importância relativa de cada entrada para a saída do neurônio.
Agora, vamos calcular a soma ponderada das entradas:
z = (x1 w1) + (x2 w2)
Se z for maior ou igual a zero, o neurônio será ativado e a função de ativação retornará 1; caso contrário, será desativado e a função de ativação retornará 0.
Vamos supor que nossas entradas sejam (x1 = 2) e (x2 = -1), e que os pesos sejam (w1 = 0.5) e (w2 = -1), como z é maior que zero, o neurônio será ativado.
Essa é uma representação simplificada de como os pesos e a função de ativação determinam se um neurônio é ativado ou não com base nas entradas que recebe.
As funções de ativação desempenham um papel crucial na representação e na transformação de informações em redes neurais. Elas não apenas introduzem não linearidades nas saídas dos neurônios, como também limitam o intervalo de valores que essas saídas podem assumir. Isso é essencial para garantir que as saídas da rede estejam dentro de um intervalo desejado e que sejam interpretáveis ou utilizáveis para a tarefa em questão.
Por exemplo, ao projetar uma rede neural para classificação de imagens em preto e branco, onde os valores de pixel podem variar de 0 (preto) a 255 (branco), é importante que a saída da rede esteja dentro do intervalo de 0 a 1, para que possa ser interpretada como uma probabilidade (por exemplo, a probabilidade de uma imagem pertencer a uma determinada classe).
Quando eu comecei a realizar alguns experimentos práticos, deparei-me com uma situação em que meu modelo de rede neural estava tendo dificuldades em encontrar a função de ativação adequada. Essa dificuldade estava afetando negativamente o desempenho do modelo, resultando em previsões imprecisas. Ao investigar mais a fundo, percebi que uma possível razão para isso era o pré-processamento inadequado dos dados que estava sendo realizado antes do treinamento da rede neural.
from sklearn import datasets
from sklearn.preprocessing import MinMaxScaler
import matplotlib.pyplot as plt
# Carregando o conjunto de dados
iris_dataset = datasets.load_iris()
iris_features = [0, 1]
iris_data = iris_dataset.data[:, iris_features]
# Criando o gráfico de dispersão
plt.scatter(iris_dataset.data[:, 0], iris_dataset.data[:, 1], c=iris_dataset.target)
plt.xlabel(iris_dataset.feature_names[0])
plt.ylabel(iris_dataset.feature_names[1])
plt.show()
# Pré-processar os dados
scaler = MinMaxScaler()
data = scaler.fit_transform(iris_data)
plt.scatter(data[:, 0], data[:, 1], c=iris_dataset.target)
plt.xlabel(iris_dataset.feature_names[0])
plt.ylabel(iris_dataset.feature_names[1])
plt.show()
Nesse trecho de código, você pode observar o pré-processamento dos dados usando MinMaxScaler()
após a visualização do gráfico de dispersão. Quanto usei RobustScaler()
o modelo não obtive nenhum resultado satisfatório, o modelo tinha uma escala de 3 a -3, e não conseguia classificar os dados, mais especificamente na parte em que verde e vermelho se encontram.
Durante os meus estudos, percebi que as funções de ativação transformam a soma ponderada dos dados de entrada e seus respectivos pesos em um número facilmente mensurável pelo computador, geralmente entre -1 e 1. Quando usei o MinMaxScaler
, observei que meu modelo alcançou o resultado com menos neurônios e camadas. Sempre que possível, NORMALIZE OS DADOS. É função do programador entender que o computador NÃO É MÁGICO. No meu caso, mesmo com uma quantidade gigantesca de neurônios (5000) e mais de 6 camadas, sem a normalização adequada dos dados, o computador teve dificuldade em aproximar uma classificação.
As funções de ativação devem ser escolhidas com base no problema a ser solucionado. Por exemplo, a função sigmoid é útil para problemas de classificação binária, onde é preciso produzir uma saída entre 0 e 1, interpretada como a probabilidade de pertencer a uma classe específica.
A escolha da função de ativação é crucial, pois diferentes funções têm propriedades distintas que podem afetar significativamente o desempenho e a capacidade de aprendizado da rede neural. A função sigmoid é preferida em problemas de classificação binária por sua capacidade de produzir saídas na forma de probabilidades. Entretanto, pode apresentar o problema de saturação do gradiente em camadas profundas, dificultando o treinamento eficaz da rede.
Com os dados corretamente normalizados, usando essa rede obtive um resultado extremamente satisfatório após aproximadamente 1500 épocas.
Classificação de Íris com Rede Neural em PyTorch
Rede Neural:
input_size = data.shape[1] # Define o tamanho da entrada com base nos dados
hidden_size1 = 50 # Define o número de neurônios na primeira camada oculta
out_size = len(iris_dataset.target_names) # Define o número de neurônios na camada de saída (número de classes)
net = nn.Sequential(
nn.Linear(input_size, hidden_size1), # Camada linear de entrada para a primeira camada oculta
nn.LeakyReLU(), # Função de ativação Leaky ReLU na primeira camada oculta
nn.Linear(hidden_size1, out_size), # Camada linear da primeira camada oculta para a camada de saída
nn.Softmax() # Função de ativação Softmax na camada de saída para probabilidades de classe
)
Treinamento:
# Importar bibliotecas
from torchsummary import summary
from IPython.display import clear_output
from torch import optim
import torch
# Carregar e preparar o conjunto de dados Iris (substitua pela sua lógica de carregamento de dados)
targets = iris_dataset.target
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# Mover a rede para o dispositivo escolhido (GPU ou CPU)
net = net.to(device)
# Definir dados de treinamento e alvos como tensores
X = torch.FloatTensor(data)
Y = torch.LongTensor(targets)
# Mover dados e alvos para o dispositivo escolhido
X = X.to(device)
Y = Y.to(device)
# Definir a função de perda (CrossEntropyLoss para classificação multiclasse)
criterion = nn.CrossEntropyLoss()
# Criar um otimizador usando SGD com taxa de aprendizado e decaimento de peso
optimizer = optim.SGD(net.parameters(), lr=0.01, weight_decay=0.01)
# Laço de treinamento (100 épocas)
for epoch in range(100):
# Passagem direta: obter previsões e calcular a perda
pred = net(X)
loss = criterion(pred, Y)
# Passagem reversa: propagar gradientes
loss.backward()
# Atualizar os parâmetros do modelo
optimizer.step()
# Função para plotar o progresso do treinamento a cada época (opcional)
def plot_10():
if epoch % 1 == 0: # Atualizar o gráfico a cada época
clear_output(wait=True) # Limpar o gráfico anterior (específico do IPython)
plt.figure()
plot_sepal(data, targets, net.to('cpu')) # Mover o modelo para CPU para plotagem
# Plotar o progresso do treinamento (opcional)
plot_10()
# Mover o modelo de volta para o dispositivo para continuação do treinamento
net = net.to(device)
# Imprimir resumo da arquitetura da rede
summary(net, input_size=(input_size,))