Machine Learning

Backpropagation do zero com Python e NumPy: uma rede neural para o problema XOR

Uma leitura passo a passo de uma rede neural 2–5–1 que implementa forward pass, propagação do erro e atualização dos pesos diretamente com operações matriciais do NumPy.

Anderson B Silva Anderson B Silva Desenvolvimento, automação e observabilidade
8 min de leitura

Backpropagation costuma aparecer por trás de bibliotecas de alto nível, mas sua mecânica essencial pode ser observada com poucas operações: multiplicar matrizes, aplicar uma função não linear, medir o erro, propagá-lo no sentido inverso e ajustar os pesos. O projeto Python-backpropagation apresenta esse fluxo em Python, usando NumPy e o problema XOR como exemplo supervisionado.

Esta publicação explica o que aparece na demonstração: os dados, a arquitetura, as duas funções de ativação e o laço de treinamento. As capturas disponíveis mostram o código, mas não exibem saídas numéricas; por isso, não serão feitas afirmações sobre convergência, acurácia ou desempenho.

Ver repositório Ver página do projeto

O problema XOR

XOR, ou “ou exclusivo”, retorna 1 quando suas duas entradas binárias são diferentes e retorna 0 quando elas são iguais. O código usa as quatro combinações possíveis como conjunto de treinamento completo.

Entrada A Entrada B Saída esperada
000
011
101
110

O interesse didático do XOR está no fato de que uma única separação linear não resolve sua tabela-verdade. Uma camada intermediária acompanhada de uma ativação não linear permite representar relações que não cabem em uma fronteira linear simples.

X = np.array([[0, 0],
              [0, 1],
              [1, 0],
              [1, 1]])

y = np.array([[0], [1], [1], [0]])

Arquitetura 2–5–1 e dimensões

A rede mostrada tem duas entradas, cinco neurônios ocultos e uma saída. Os quatro exemplos são processados juntos em cada iteração, de modo que entradas, ativações e saídas são representadas por matrizes.

Elemento Dimensão Papel
X4 × 2Quatro exemplos com duas características
pesos_entrada_oculta2 × 5Conecta as entradas à camada oculta
camada_oculta4 × 5Ativações ocultas para cada exemplo
pesos_oculta_saida5 × 1Conecta a camada oculta à saída
camada_saida4 × 1Uma previsão para cada combinação
X (4 × 2)
  → pesos_entrada_oculta (2 × 5)
  → camada_oculta (4 × 5)
  → pesos_oculta_saida (5 × 1)
  → camada_saida (4 × 1)

As matrizes contêm dez pesos entre entrada e camada oculta e cinco entre camada oculta e saída, totalizando 15 pesos treináveis. Essa contagem deriva diretamente das dimensões. No código demonstrado não aparecem vetores de bias.

Forward pass: da entrada à previsão

No forward pass, a entrada é multiplicada pela primeira matriz de pesos. A função de ativação transforma o resultado e produz a camada oculta. A mesma sequência se repete entre a camada oculta e a saída.

camada_oculta = f(np.dot(X, pesos_entrada_oculta))
camada_saida = f(np.dot(camada_oculta, pesos_oculta_saida))

Em notação compacta, o fluxo é H = f(X · W_ih) e Ŷ = f(H · W_ho). A função f é sigmoide em uma variante e tangente hiperbólica na outra.

Por que as dimensões importam

O produto (4 × 2) · (2 × 5) produz (4 × 5). Em seguida, (4 × 5) · (5 × 1) produz (4 × 1), exatamente uma saída para cada linha de XOR.

Erro e backpropagation

Depois do forward pass, o script calcula o residual com erro = y - camada_saida. Esse sinal forma o delta da saída em conjunto com a derivada da ativação. O erro é então projetado de volta para a camada oculta pela transposta da matriz de pesos da saída.

erro = y - camada_saida

d_saida = erro * derivada(camada_saida)
erro_oculta = d_saida.dot(pesos_oculta_saida.T)
d_oculta = erro_oculta * derivada(camada_oculta)

pesos_oculta_saida += camada_oculta.T.dot(d_saida) * learning_rate
pesos_entrada_oculta += X.T.dot(d_oculta) * learning_rate

O delta da saída combina o residual com a sensibilidade local da ativação. Depois, pesos_oculta_saida.T transporta essa informação para trás. Por fim, os produtos com camada_oculta.T e X.T reúnem as contribuições dos quatro exemplos e atualizam as duas matrizes.

Sigmoide e tangente hiperbólica

Variante com sigmoide

A sigmoide é implementada como 1 / (1 + np.exp(-x)) e produz valores no intervalo aberto entre zero e um. Sua derivada é calculada a partir do valor já ativado com x * (1 - x).

Variante com tangente hiperbólica

A segunda versão usa np.tanh(x) e 1 - x**2 como derivada sobre o valor ativado. Dados, dimensões, inicialização, taxa de aprendizado e número de iterações permanecem iguais na captura.

A tanh não é uma função exclusiva de regressão. Nesta demonstração, os alvos continuam codificados como 0 e 1; comparar essa escolha com uma codificação centrada em -1 e 1 seria uma análise adicional, não um resultado apresentado pelo projeto.

Inicialização e treinamento

O código define np.random.seed(42), inicializa os pesos com valores uniformes entre -1 e 1, usa taxa de aprendizado 0.3 e executa 20.000 iterações.

Configuração Valor exibido
Semente pseudoaleatória42
Faixa inicial dos pesos-1 a 1
Taxa de aprendizado0.3
Iterações20.000
MonitoramentoA cada 1.000 iterações
np.random.seed(42)
learning_rate = 0.3

for epoch in range(20000):
    # forward pass, erro, backpropagation e atualização

    if epoch % 1000 == 0:
        print(np.mean(np.abs(erro)))

O valor monitorado é a média do erro absoluto naquele estado do treinamento. Ele ajuda a acompanhar a magnitude média do residual, mas, isoladamente, não é uma declaração de acurácia. O script também imprime a última camada_saida após o laço, embora os valores produzidos não apareçam nas imagens analisadas.

O que a demonstração comprova

As duas capturas comprovam que o exemplo contém os dados de XOR, uma rede 2–5–1, forward pass, cálculo do residual, retropropagação, atualização dos pesos e duas opções de ativação. Também mostram que o código foi preparado para imprimir o erro periodicamente e a saída final.

Limite factual da demonstração

As imagens não mostram terminal, previsões finais, curva de erro, acurácia, tempo de execução ou comparação quantitativa entre as ativações. Portanto, esta publicação não afirma que uma variante convergiu ou foi superior à outra.

Análise: limitações e evoluções possíveis

As observações a seguir são uma análise técnica do código exibido, não funcionalidades ou resultados declarados pelo projeto.

  • Adicionar vetores de bias às duas camadas.
  • Definir um critério de parada baseado no erro, em vez de sempre executar 20.000 iterações.
  • Registrar o histórico do erro para produzir uma curva de treinamento.
  • Executar as duas ativações sob o mesmo protocolo e documentar os resultados antes de compará-las.
  • Separar dados, forward pass, treinamento e avaliação em funções testáveis.
  • Avaliar uma formulação numericamente estável da sigmoide para valores extremos.

Não há separação entre treino e teste no trecho mostrado: as quatro linhas de XOR são usadas juntas. Para esta demonstração, elas também representam todas as combinações possíveis de duas entradas binárias, mas isso não constitui evidência de generalização para outros conjuntos de dados.

Acessar o GitHub Conhecer o projeto
Anderson B Silva
Anderson B Silva

Desenvolvedor focado em automação, observabilidade e soluções que simplificam operações complexas.

Voltar ao Blog