← Python · tijolo 05

A rede inteira — XOR com retropropagação

XOR e retropropagação

Esta não é um tijolo do curso: é o destino deles. É a rede inteira, com os quatro tijolos já encaixados. Guardada aqui para comparar quando você chegar lá pelo caminho longo.

A rede que aprende sozinha

O XOR é o exemplo clássico porque não dá para resolver com um neurônio só. Não existe uma reta que separe [0,1] e [1,0] de [0,0] e [1,1]. Por isso aparece a camada oculta: ela dobra o espaço até o problema virar separável.

Os quatro passos do ciclo:

  1. Propagação — a entrada atravessa a rede e vira um palpite
  2. Erro — quanto o palpite errou do esperado
  3. Retropropagação — o erro volta para a camada oculta, repartido proporcionalmente aos pesos que o causaram
  4. Atualização — cada peso anda um passinho na direção que diminui o erro
import math
import random

def sigmoid(x):
    return 1 / (1 + math.exp(-x))

def derivada_sigmoid(y):
    # A derivada recebe a saída já calculada pela sigmoid
    return y * (1 - y)

# 1. DADOS DE TREINAMENTO: O problema XOR (Não-linear)
# Diferente do AND/OR, o XOR requer que UMA e apenas UMA entrada seja verdadeira.
treino = [
    ([0, 0], 0),
    ([0, 1], 1),
    ([1, 0], 1),
    ([1, 1], 0), 
]

# 2. INICIALIZAÇÃO DA REDE (Pesos e Vieses)
# Camada Oculta: 2 entradas -> 2 neurônios ocultos
w_ocultos = [[random.uniform(-1, 1) for _ in range(2)] for _ in range(2)]
b_ocultos = [random.uniform(-1, 1) for _ in range(2)]

# Camada de Saída: 2 entradas (da oculta) -> 1 neurônio de saída
w_saida = [random.uniform(-1, 1) for _ in range(2)]
b_saida = random.uniform(-1, 1)

taxa_aprendizado = 0.5
epocas = 20000

# 3. CICLO DE TREINAMENTO COMPLETO
for epoca in range(epocas):
    for x, esperado in treino:
        # --- PASSO 1: PROPAGAÇÃO (Forward) ---
        # Calcular ativação da Camada Oculta
        saida_oculta = [0.0, 0.0]
        for i in range(2):
            z = sum(x[j] * w_ocultos[i][j] for j in range(2)) + b_ocultos[i]
            saida_oculta[i] = sigmoid(z)

        # Calcular ativação da Camada de Saída
        z_saida = sum(saida_oculta[j] * w_saida[j] for j in range(2)) + b_saida
        saida_final = sigmoid(z_saida)

        # --- PASSO 2: CÁLCULO DO ERRO ---
        erro_saida = esperado - saida_final
        gradiente_saida = erro_saida * derivada_sigmoid(saida_final)

        # --- PASSO 3: RETROPROPAGAÇÃO (Backpropagation) ---
        # O erro é repassado para a camada oculta proporcionalmente aos pesos
        erro_oculto = [0.0, 0.0]
        gradiente_oculto = [0.0, 0.0]
        for i in range(2):
            erro_oculto[i] = gradiente_saida * w_saida[i]
            gradiente_oculto[i] = erro_oculto[i] * derivada_sigmoid(saida_oculta[i])

        # --- PASSO 4: ATUALIZAÇÃO DOS PESOS ---
        # Ajustar pesos da saída
        for i in range(2):
            w_saida[i] += taxa_aprendizado * gradiente_saida * saida_oculta[i]
        b_saida += taxa_aprendizado * gradiente_saida

        # Ajustar pesos ocultos
        for i in range(2):
            for j in range(2):
                w_ocultos[i][j] += taxa_aprendizado * gradiente_oculto[i] * x[j]
            b_ocultos[i] += taxa_aprendizado * gradiente_oculto[i]

# 4. TESTE DA REDE APÓS O TREINAMENTO
print("Resultados do Teste XOR após treinamento:")
for x, esperado in treino:
    saida_oculta = [sigmoid(sum(x[j] * w_ocultos[i][j] for j in range(2)) + b_ocultos[i]) for i in range(2)]
    saida_final = sigmoid(sum(saida_oculta[j] * w_saida[j] for j in range(2)) + b_saida)
    print(f"Entrada: {x} -> Previsão: {saida_final:.4f} (Esperado: {esperado})")

O que deu quando rodou

Resultados do Teste XOR após treinamento:
Entrada: [0, 0] -> Previsão: 0.0107 (Esperado: 0)
Entrada: [0, 1] -> Previsão: 0.9889 (Esperado: 1)
Entrada: [1, 0] -> Previsão: 0.9889 (Esperado: 1)
Entrada: [1, 1] -> Previsão: 0.0136 (Esperado: 0)

Trinta centésimos de segundo, 20 mil épocas. Aprendeu.

A armadilha: ela não aprende sempre

Rodei 40 vezes seguidas. Aprendeu em 34. As 6 que falharam falharam todas do mesmo jeito:

[0.011, 0.484, 0.985, 0.484]
[0.011, 0.492, 0.989, 0.492]
[0.011, 0.989, 0.492, 0.492]

Repare no padrão: ela acerta dois casos e trava em ~0.49 nos outros dois — cima do muro, sem decidir. Isso é mínimo local: dependendo dos pesos sorteados no começo, o gradiente desce para um buraco do qual não consegue mais sair, e nenhuma época a mais resolve.

Não é bug no código. É uma propriedade de uma rede com só 2 neurônios ocultos. Os três remédios de sempre:

Se quiser reproduzir a medição, é este script:

# Roda a mesma rede 40 vezes e conta quantas realmente aprenderam.
import subprocess, re

ok = 0
falhas = []
for n in range(40):
    saida = subprocess.run(['python3', 'xor.py'], capture_output=True, text=True).stdout
    nums = [float(x) for x in re.findall(r'Previsão: ([\d.]+)', saida)]
    esperados = [0, 1, 1, 0]
    # "aprendeu" = todas as 4 previsões do lado certo de 0.5
    acertou = all((p > 0.5) == (e == 1) for p, e in zip(nums, esperados))
    if acertou:
        ok += 1
    else:
        falhas.append([round(p, 3) for p in nums])

print(f"aprendeu em {ok} de 40 rodadas")
if falhas:
    print("as que empacaram:")
    for f in falhas[:6]:
        print("  ", f)

Livraria do Neon · estante redes-neurais · o texto e o código são do João; o que entra de novo aqui é a arrumação para a web