LABORATÓRIO 03 · XOR

Por que uma reta não resolve XOR?

Compare um Perceptron de uma camada com uma MLP. As regiões coloridas mostram, ao vivo, a decisão aprendida por cada arquitetura.

Decisão da MLP para XOR

Cada ponto representa uma combinação da tabela-verdade.

Rede nova
Saída 0Saída 1Fronteira aprendida
Épocas0
Acertos
Erro médio

Neurônios da MLP · 2 → 4 → 1

Os valores dentro dos círculos são as ativações do forward pass.

Selecione uma entrada de teste para acompanhar seu caminho pela rede.
1 · Potencialu = Σ(wᵢxᵢ) + b
2 · Ativaçãoa = g(u)
3 · Forwardentrada → oculta → ŷ
4 · Erroe = y − ŷ
5 · Backpropgradientes voltam pela rede
6 · Atualizaçãow ← w − η∇w

Passo a passo matemático

Forward, deltas e pesos da última amostra.

Treine uma época para visualizar as contas.

Erro × resposta

Compare a queda do erro com o aumento da taxa de acertos.

Erro médioTaxa de acerto

Duas regiões são necessárias

Os resultados 1 ocupam cantos opostos. Uma única reta sempre deixa pelo menos um ponto do lado errado. A camada oculta permite combinar várias retas e formar uma decisão não linear.

LEITURA DO MODELO

Perceptron

Calcula u = w₁x₁ + w₂x₂ + b e produz ŷ = Step(u). Há apenas uma fronteira: u = 0.

MLP

Cada neurônio oculto calcula seu próprio potencial u. A combinação dessas ativações cria as duas regiões exigidas pelo XOR.

CÓDIGO DIDÁTICO

MLP simples para resolver XOR

A camada oculta possui dois neurônios. O código separa forward pass, cálculo dos gradientes, backpropagation e atualização dos pesos.

sigmoid()criarRede()forward()backpropagation()atualizarPesos()treinar()
mlp-xor.jsexemplo simplificado
// Tabela-verdade completa do XOR.
const dadosXOR = [
  { x: [0, 0], y: 0 },
  { x: [0, 1], y: 1 },
  { x: [1, 0], y: 1 },
  { x: [1, 1], y: 0 }
];

// Sigmoid comprime qualquer potencial u para o intervalo de 0 a 1.
function sigmoid(u) {
  return 1 / (1 + Math.exp(-u));
}

// Calcula a derivada usando a ativação já obtida no forward pass.
function derivadaSigmoid(ativacao) {
  return ativacao * (1 - ativacao);
}

// A semente fixa torna o exemplo reproduzível em qualquer execução.
let semente = 1;

// Gera um peso pseudoaleatório pequeno a partir da semente.
function pesoAleatorio() {
  semente = (semente * 16807) % 2147483647;
  return (semente / 2147483647) * 2 - 1;
}

// Cria uma arquitetura 2 → 2 → 1.
function criarRede() {
  return {
    oculta: [
      { pesos: [pesoAleatorio(), pesoAleatorio()], bias: pesoAleatorio() },
      { pesos: [pesoAleatorio(), pesoAleatorio()], bias: pesoAleatorio() }
    ],
    saida: {
      pesos: [pesoAleatorio(), pesoAleatorio()],
      bias: pesoAleatorio()
    }
  };
}

// Multiplica entradas por pesos e acrescenta o bias.
function potencial(entradas, pesos, bias) {
  return entradas.reduce(
    (soma, entrada, indice) => soma + entrada * pesos[indice],
    bias
  );
}

// Executa somente o forward pass; nenhum peso é alterado aqui.
function forward(entradas, rede) {
  const oculta = rede.oculta.map(neuronio => {
    const u = potencial(entradas, neuronio.pesos, neuronio.bias);
    const a = sigmoid(u);
    return { u, a };
  });

  const uSaida = potencial(
    oculta.map(neuronio => neuronio.a),
    rede.saida.pesos,
    rede.saida.bias
  );

  const probabilidade = sigmoid(uSaida);
  return { oculta, uSaida, probabilidade };
}

// Usa o erro da saída para calcular os deltas de todas as camadas.
function backpropagation(entradas, y, rede, resultado) {
  // Com Sigmoid + entropia cruzada, o delta da saída é ŷ - y.
  const deltaSaida = resultado.probabilidade - y;

  // Cada neurônio oculto recebe uma parte do erro da saída.
  const deltasOcultos = resultado.oculta.map((neuronio, indice) =>
    deltaSaida *
    rede.saida.pesos[indice] *
    derivadaSigmoid(neuronio.a)
  );

  return { entradas, deltaSaida, deltasOcultos, ativacoes: resultado.oculta };
}

// Aplica descida do gradiente depois que todos os deltas foram calculados.
function atualizarPesos(rede, gradientes, taxa) {
  rede.saida.pesos = rede.saida.pesos.map(
    (peso, indice) =>
      peso - taxa * gradientes.deltaSaida * gradientes.ativacoes[indice].a
  );
  rede.saida.bias -= taxa * gradientes.deltaSaida;

  rede.oculta.forEach((neuronio, indiceOculto) => {
    neuronio.pesos = neuronio.pesos.map(
      (peso, indiceEntrada) =>
        peso -
        taxa *
        gradientes.deltasOcultos[indiceOculto] *
        gradientes.entradas[indiceEntrada]
    );
    neuronio.bias -= taxa * gradientes.deltasOcultos[indiceOculto];
  });
}

// Reúne forward, erro, backpropagation e atualização para uma amostra.
function treinarAmostra(amostra, rede, taxa) {
  const resultado = forward(amostra.x, rede);
  const gradientes = backpropagation(amostra.x, amostra.y, rede, resultado);
  atualizarPesos(rede, gradientes, taxa);
}

// Repete todas as amostras por várias épocas.
function treinar(dados, epocas, taxa) {
  const rede = criarRede();

  Array.from({ length: epocas }).forEach(() => {
    dados.forEach(amostra => treinarAmostra(amostra, rede, taxa));
  });

  return rede;
}

// A decisão final usa limiar de probabilidade igual a 0,5.
function prever(entradas, rede) {
  return forward(entradas, rede).probabilidade >= 0.5 ? 1 : 0;
}

const redeTreinada = treinar(dadosXOR, 10000, 0.7);
dadosXOR.forEach(amostra => {
  console.log(amostra.x, prever(amostra.x, redeTreinada));
});