← Python · tijolo 05
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.
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:
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})")
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.
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