No post anterior, exploramos o Perceptron: vimos que ele é capaz de aprender e tomar decisões simples, mas também descobrimos que ele tem um limite bem definido. Por ser um classificador linear, ele só consegue resolver problemas onde os dados podem ser separados por uma linha reta.
A limitação mais emblemática na história das redes neurais foi o operador lógico XOR (OU-Exclusivo). Como os pontos desse problema não podem ser divididos por uma única reta, um Perceptron sozinho entra em loop infinito e nunca aprende a regra.
Para superar esse obstáculo, duas frentes precisaram evoluir: a organização dos neurônios e a forma como eles “disparam” a informação. Neste post, vamos entender como os Perceptrons Multicamadas (Multilayer Perceptron - MLP) resolvem essa complexidade.
Introdução às Redes Multicamada
Um Perceptron Multicamada (MLP) nada mais é do que um conjunto de Perceptrons organizados em uma estrutura mais robusta. Enquanto o modelo original tinha apenas entradas e uma saída direta, o MLP introduz um elemento crucial: as camadas escondidas.
Estrutura
Em uma rede multicamada, os neurônios são organizados em três tipos de camadas:
- Camada de Entrada: é por onde os dados (as características) entram na rede.
- Camadas Escondidas (Hidden Layers): onde o processamento intermediário acontece. Uma rede pode ter uma ou várias dessas camadas.
- Camada de Saída: é onde a rede entrega a resposta final.
O ponto mais importante aqui é que cada neurônio na camada escondida faz exatamente os mesmos cálculos que um Perceptron comum: ele recebe as entradas, multiplica pelos pesos e soma tudo.
Camada escondida
Podemos entender a camada escondida como sendo um “tradutor”: ela recebe os dados brutos e os reescreve de uma forma que a camada de saída consiga entender.
Se o problema do XOR não pode ser resolvido com uma linha reta nos dados originais, a camada escondida aprende uma representação intermediária desses dados. Nessa nova representação, a camada de saída consegue finalmente separar as classes.
Feedforward
O processo de a informação entrar, passar pelas camadas escondidas e chegar à saída é chamado de feedforward (alimentação para frente). É um fluxo de apenas um sentido: cada camada processa os valores e os repassa para a próxima.
Funções de Ativação
Como vimos no Perceptron, após somar as entradas e pesos, o neurônio precisa decidir qual valor será passado adiante. Esse “filtro” é a Função de Ativação.
Existem inúmeras funções de ativação, cada uma mais indicada para um cenário específico, e as três mais comuns serão abordadas a seguir.
Função Step (Degrau)
Usada no perceptron clássico, a função degrau é binária: se a soma for maior que um limiar, ela retorna 1. Caso contrário, retorna 0. Embora simples, ela é muito rígida para redes com várias camadas, já que não permite nuances no aprendizado.
Função Sigmoide
Diferente da função degrau, a Sigmoide gera uma curva suave, retornando valores entre 0 e 1. Isso significa que o neurônio não apenas “dispara ou não”, mas transmite um nível de intensidade.
A fórmula da Sigmoide é: $$ y = \frac{1}{1 + e^{-x}} $$ Onde $e$ é o Número de Euler, uma constante matemática aproximadamente igual a $2,718$, usada na natureza e na computação para descrever processos de crescimento. Na prática:
- Se $x$ for um valor alto, a Sigmoide retorna algo próximo a 1.
- Se $x$ for um valor muito baixo, ela retorna algo próximo a 0.
- Ela nunca retorna valores negativos.
Em Python, podemos implementá-la usando a biblioteca NumPy:
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
Por conta de sua clareza e bons resultados em exemplos didáticos, esta é a função que será utilizada neste post.
Função Tangente Hiperbólica (Tanh)
Muito similar à Sigmoide, mas com uma diferença importante: a Tangente Hiperbólica retorna valores entre -1 e 1, o que pode ser útil em cenários onde valores negativos ajudam a rede a aprender mais rápido. $$ Y = \frac{e^x - e^{-x}}{e^x + e^{-x}} $$
Prática
Para visualizar o fluxo de dados em uma rede multicamada, vamos implementar o processo de feedforward. Neste exemplo, definiremos pesos manuais para observar como a rede processa as entradas do XOR e como quantificamos o erro da saída.
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# Conjunto de entradas (XOR)
inputs = np.array([
[0, 0],
[0, 1],
[1, 0],
[1, 1]
])
# Resultados esperados
expected_outputs = np.array([[0], [1], [1], [0]])
# Matriz de Pesos: Entrada -> Camada Oculta (2 entradas -> 3 neurônios)
weights_input_hidden = np.array([
[0.234, -0.740, -0.371],
[0.435, 0.547, -0.469]
])
# Matriz de Pesos: Camada Oculta -> Saída (3 neurônios -> 1 saída)
weights_hidden_output = np.array([
[0.107],
[-0.893],
[0.130]
])
if __name__ == '__main__':
# 1. Processamento da Camada Oculta
# Produto escalar das entradas pelos pesos seguido da ativação sigmoide
hidden_layer_input = np.dot(inputs, weights_input_hidden)
hidden_layer_output = sigmoid(hidden_layer_input)
# 2. Processamento da Camada de Saída
# O resultado da camada oculta serve como entrada para a camada final
output_layer_input = np.dot(hidden_layer_output, weights_hidden_output)
predictions = sigmoid(output_layer_input)
# 3. Avaliação do Erro
# Diferença entre o valor esperado e o valor previsto pela rede
error = expected_outputs - predictions
# Cálculo do Erro Médio Absoluto (MAE)
mean_absolute_error = np.mean(np.abs(error))
print(f"Previsões da rede:\n{predictions}")
print(f"\nErro Médio Absoluto: {mean_absolute_error:.4f}")
Arquitetura e Matrizes de Pesos
Diferente do Perceptron tradicional, o MLP utiliza matrizes de pesos para conectar as diferentes camadas. As dimensões dessas matrizes definem a estrutura da rede:
weights_input_hidden(2x3): indica que cada uma das 2 entradas está conectada a cada um dos 3 neurônios da camada oculta, e cada valor na matriz representa o peso dessa conexão.weights_hidden_output(3x1): indica que os 3 neurônios da camada oculta convergem suas saídas para um único neurônio final.
Cálculo do Erro
Após a execução do feedforward, comparamos os resultados obtidos com os valores reais. Para medir a performance global da rede, utilizamos o Erro Médio Absoluto, em dois passos:
np.abs(error): o valor absoluto é necessário para garantir que erros em direções opostas (positivos e negativos) não se anulem. Para isso, transformamos cada diferença em uma medida de distância positiva.np.mean(...): a média dessas distâncias fornece um indicador numérico da precisão da rede em todo o conjunto de dados.
A medição do erro é o requisito fundamental para o aprendizado da rede neural. Nesse exemplo, os pesos são estáticos, ou seja, a rede não “aprende”, apenas processa dados com valores pré-definidos.
No próximo post, a magnitude desse erro será usada para recalcular os pesos da rede a cada iteração — é isso que permite que ela caminhe para a solução de problemas não-lineares como o XOR.