Objetivo da Aula
Implementar uma rede neural do zero que resolve XOR, entendendo por que uma camada única falha
Descrever o ciclo completo de ML: dados → arquitetura → forward pass → loss → backpropagation → inferência
Interpretar curvas de loss durante o treino para identificar convergência, divergência e overfitting
Explicar o que são funções de ativação e por que sigmoide torna XOR solucionável
Conectar os princípios de XOR com o que acontece em LLMs com bilhões de parâmetros
Por que isso importa
XOR parece trivial. É o “Hello World” do Deep Learning por uma razão profunda: é o problema mínimo que demonstra por que redes de uma camada falham e por que hidden layers são necessárias.
Quando você entender por que XOR não pode ser resolvido com uma linha reta, você vai entender: - Por que LLMs têm dezenas de camadas (cada uma aprendendo representações mais abstratas) - Por que fine-tuning funciona: as camadas iniciais aprenderam representações universais (como resolver XOR), e as camadas finais especializam para o domínio - Por que aumentar parâmetros resolve problemas mais complexos — mas só até certo ponto
Impacto direto no seu trabalho: quando você decide entre usar um modelo menor vs maior, está fazendo exatamente o mesmo trade-off do XOR. Um modelo de 2 neurônios não resolve XOR. Um de 4 resolve. Um de 100 resolve mas é mais lento e caro. Sempre existe um tamanho mínimo suficiente para um problema — a arte está em encontrá-lo.
Conceitos Fundamentais
O problema XOR: por que é especial?
XOR (exclusive OR) produz 1 quando exatamente uma das entradas é 1:
Entrada A | Entrada B | Saída XOR
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0Plotado em 2D, os quatro pontos ficam assim:
Entrada B
1 | ●(0,1)=1 ○(1,1)=0
|
0 | ○(0,0)=0 ●(1,0)=1
+-----------------
0 1 Entrada A
○ = saída 0
● = saída 1Fundamento: Separabilidade e Funções de Ativação
Classificação é traçar uma fronteira entre grupos. Uma rede linear só consegue traçar fronteiras retas — qualquer combinação linear de entradas ainda é uma função linear. Para traçar fronteiras curvas (como separar os casos do XOR), você precisa de não-linearidade: a função de ativação "dobra" o espaço depois de cada camada, permitindo que a próxima camada trace uma fronteira no espaço já dobrado. Uma rede com N camadas empilha N dobramentos. É o mesmo princípio por trás dos dezenas de camadas de atenção e feed-forward de um LLM: cada camada refina a representação no espaço de embeddings, tornando padrões progressivamente mais abstratos separáveis.
Por que uma linha reta não funciona? Um perceptron (neurônio único) aprende um hiperplano — em 2D, uma linha reta — que separa as duas classes. Você pode tentar todas as linhas retas imagináveis e nenhuma vai separar os ○ dos ●. Os pontos não são linearmente separáveis.
Uma rede com hidden layer resolve porque transforma o espaço. Pensa assim: a rede aprende a “dobrar” o espaço de forma que os pontos se tornam separáveis em uma dimensão superior.
Anatomia de uma rede neural
Rede para XOR com 1 hidden layer de 4 neurônios:
Input Layer Hidden Layer Output Layer
[A] ─────► [H1] ─────►
[H2] ─────►
[B] ─────► [H3] ─────► [Y]
[H4] ─────►
2 inputs → 4 neurônios ocultos → 1 output
Parâmetros: (2×4 pesos + 4 bias) + (4×1 peso + 1 bias) = 17 parâmetros no totalCada conexão tem um peso (número real, inicializado aleatoriamente). O treinamento ajusta esses pesos para minimizar o erro.
Forward Pass: dados fluindo pela rede
No forward pass, dados entram pelo input e fluem até o output:
Para o exemplo (0, 1) → esperado: 1
Passo 1: multiplicação de pesos (para cada neurônio oculto Hᵢ)
Hᵢ_bruto = A × w_A_Hᵢ + B × w_B_Hᵢ + bias_Hᵢ
Passo 2: função de ativação (por que não linear?)
Hᵢ_ativo = sigmoide(Hᵢ_bruto) = 1 / (1 + e^(-Hᵢ_bruto))
Passo 3: output layer
Y_bruto = H1×w1 + H2×w2 + H3×w3 + H4×w4 + bias_Y
Y = sigmoide(Y_bruto)
Resultado: um número entre 0 e 1 (ex: 0.92)Por que sigmoide? Sem função de ativação não-linear, múltiplas camadas lineares colapsam para uma única camada linear (composição de funções lineares é linear). Sigmoide “dobra” o espaço em cada camada, permitindo representações não-lineares.
Loss: medindo o erro
Loss quantifica quão errada está a predição:
MSE (Mean Squared Error):
Loss = (predição - esperado)²
Exemplos:
predição = 0.92, esperado = 1.0 → Loss = (0.92 - 1.0)² = 0.0064 ✓ bom
predição = 0.50, esperado = 1.0 → Loss = (0.50 - 1.0)² = 0.2500 ✗ ruim
predição = 0.08, esperado = 1.0 → Loss = (0.08 - 1.0)² = 0.8464 ✗ muito ruimDurante o treino, a loss deve diminuir monotonicamente (com flutuações). Se aumentar, algo está errado (learning rate muito alto, dados com bug, arquitetura inadequada).
Backpropagation: aprendendo com o erro
Backpropagation calcula como cada peso contribuiu para o erro, usando a regra da cadeia do cálculo:
∂Loss/∂peso = ∂Loss/∂output × ∂output/∂peso
Para pesos na última camada: cálculo direto
Para pesos em camadas anteriores: produto das derivadas de todas as camadas posterioresO gradiente ∂Loss/∂peso diz: “se aumentarmos este peso em ε, a loss aumenta ou diminui?”
Após calcular gradientes, atualizamos os pesos:
peso_novo = peso_antigo - learning_rate × gradiente
learning_rate (taxa de aprendizado):
Muito grande: pesos oscilam, loss não converge
Muito pequena: convergência muito lenta
Típico: 0.001 a 0.01 para a maioria dos problemasEpoch e convergência
Uma epoch é uma passagem completa pelos dados de treino. Para XOR com 4 exemplos:
Epoch 1: [0,0], [0,1], [1,0], [1,1] → loss: 0.2500
Epoch 2: [0,0], [0,1], [1,0], [1,1] → loss: 0.1823
Epoch 3: [0,0], [0,1], [1,0], [1,1] → loss: 0.1234
...
Epoch 5000: [0,0], [0,1], [1,0], [1,1] → loss: 0.0002 ✓Fundamento: Vocabulário de Treino
Uma época é uma passada completa pelo dataset de treinamento. Com 2.000 épocas, o modelo vê os mesmos dados 2.000 vezes, ajustando os pesos a cada vez. Overfitting é quando o modelo memoriza os dados de treino em vez de generalizar: o erro de treino cai, mas o erro num dataset de validação (nunca visto) sobe. Early stopping para o treino quando o erro de validação começa a subir. O seed fixa a inicialização aleatória dos pesos — sem ele, dois treinos idênticos dão redes ligeiramente diferentes. Esses conceitos voltam na Parte IX, no fine-tuning via API.
A rede XOR tipicamente converge em 2.000–10.000 épocas dependendo da inicialização aleatória dos pesos.
Aprofundamento Técnico
Por que hidden layers habilitam representações não-lineares?
Vamos rastrear o que acontece internamente numa rede XOR treinada. Após convergência, a hidden layer aprende a computar funções não-lineares que transformam o espaço:
Neurônio H1 aprende: "A e B são diferentes?"
H1(0,0) ≈ 0.1 (não muito diferentes)
H1(0,1) ≈ 0.9 (muito diferentes)
H1(1,0) ≈ 0.9 (muito diferentes)
H1(1,1) ≈ 0.1 (não muito diferentes)
Neurônio H2 aprende: "pelo menos um é 1?"
H2(0,0) ≈ 0.1
H2(0,1) ≈ 0.9
H2(1,0) ≈ 0.9
H2(1,1) ≈ 0.9
Output aprende: "H1 é alto E H2 é alto?"
→ Isso implementa XOR!Cada camada aprende uma abstração. Em LLMs com 96 camadas, as primeiras camadas aprendem padrões de tokens (similaridade lexical), camadas do meio aprendem sintaxe (estrutura de frase), camadas finais aprendem semântica e raciocínio. Essa hierarquia de representações é o que torna LLMs tão poderosos.
Inicialização aleatória e reprodutibilidade
Pesos são inicializados aleatoriamente. Isso significa que: 1. Duas execuções do mesmo código podem convergir para soluções diferentes 2. Às vezes a rede não converge (fica presa em mínimo local) e precisa ser reiniciada 3. Para reprodutibilidade, fixe o seed: Math.random.seed(42) ou equivalente no framework
Para o XOR, com brain.js e uma hidden layer de 4+ neurônios, a taxa de convergência é alta (> 95% das inicializações convergem). Com 2 neurônios, pode falhar para alguns seeds.
Overfitting vs Underfitting: o trade-off fundamental
Underfitting: modelo muito simples para o problema
→ Loss de treino alto, loss de validação alto
→ Solução: mais camadas, mais neurônios, mais épocas
Fitting correto: modelo na complexidade certa
→ Loss de treino baixo, loss de validação similar
→ É o que queremos!
Overfitting: modelo memorizou dados de treino
→ Loss de treino muito baixo, loss de validação alto
→ Solução: menos épocas, dropout, regularização, mais dadosPara XOR com apenas 4 exemplos, overfitting não é preocupação — a rede não tem “novos dados” para falhar. Em problemas reais com milhares de exemplos, overfitting é o inimigo principal.
Relação com LLMs: modelos de linguagem gigantes com bilhões de parâmetros e trilhões de tokens de treino resistem ao overfitting porque os dados superam muito os parâmetros. Mesmo assim, fine-tuning com poucos exemplos é suscetível a overfitting.
O papel da taxa de aprendizado (learning rate)
// Muito alta: loss "explode" ou oscila
// learning_rate = 10.0
// Iteração 1: loss = 0.25
// Iteração 2: loss = 0.87 ← PIOROU!
// Iteração 3: loss = 0.12
// → Oscilação, sem convergência estável
// Muito baixa: converge mas lentamente
// learning_rate = 0.0001
// Iteração 1000: loss = 0.23 ← quase não saiu do lugar
// → Precisaria de 100× mais iterações
// Adequada para XOR
// learning_rate = 0.1 (padrão do Brain.js)
// Iteração 500: loss = 0.08
// Iteração 2000: loss = 0.001
// → Convergência eficienteBrain.js usa learning rate de 0.3 por padrão — razoável para XOR. Para problemas mais complexos, learning rate scheduling (decaimento ao longo do treino) é comum.
Exemplos Anotados
Exemplo 1: Rede neural XOR completa com Brain.js
const brain = require('brain.js')
// Dados do XOR: cada exemplo tem input e output esperado
// Brain.js aceita arrays numéricos para ambos
const dadosTreino = [
{ input: [0, 0], output: [0] }, // 0 XOR 0 = 0
{ input: [0, 1], output: [1] }, // 0 XOR 1 = 1
{ input: [1, 0], output: [1] }, // 1 XOR 0 = 1
{ input: [1, 1], output: [0] }, // 1 XOR 1 = 0
]
// hiddenLayers: [4] significa uma camada com 4 neurônios
// Por que 4 e não 2? Com 2, alguns seeds não convergem.
// Com 4, convergência é mais robusta (mais "caminhos" para aprender XOR)
const rede = new brain.NeuralNetwork({
hiddenLayers: [4],
activation: 'sigmoid', // padrão — boa para classificação binária
})
// Treinamento: iterações é o número máximo de épocas
// errorThresh: para quando loss < threshold (0.005 = 0.5% de erro)
// log e logPeriod: imprime progresso a cada 500 iterações
const resultado = rede.train(dadosTreino, {
iterations: 10000,
errorThresh: 0.005,
log: true,
logPeriod: 500,
})
// resultado contém métricas do treino
console.log(`Treino concluído em ${resultado.iterations} iterações`)
console.log(`Erro final: ${resultado.error.toFixed(6)}`)
// Se error < 0.01, a rede aprendeu XOR com boa precisãoOutput esperado:
iterations: 500, training error: 0.150234
iterations: 1000, training error: 0.087123
iterations: 1500, training error: 0.034567
iterations: 2000, training error: 0.011234
iterations: 2347, training error: 0.004893 ← parou (< threshold)
Treino concluído em 2347 iterações
Erro final: 0.004893Exemplo 2: Inferência e interpretação de probabilidades
// Após treinar, rede.run() recebe um input e retorna um array de probabilidades
// (não 0/1 diretamente — você decide o threshold de decisão)
const inputs = [[0, 0], [0, 1], [1, 0], [1, 1]]
const esperados = [0, 1, 1, 0]
console.log('\n=== Resultado do XOR ===')
let acertos = 0
for (let i = 0; i < inputs.length; i++) {
const saida = rede.run(inputs[i])
// saida é um array com um elemento (nossa output layer tem 1 neurônio)
const probabilidade = saida[0]
// Math.round() aplica threshold de 0.5: >= 0.5 → 1, < 0.5 → 0
// Em problemas reais, você pode querer threshold diferente (ex: 0.7 para alta precisão)
const predicao = Math.round(probabilidade)
const correto = predicao === esperados[i]
if (correto) acertos++
console.log(
`[${inputs[i]}] → prob: ${probabilidade.toFixed(4)} → pred: ${predicao} → esperado: ${esperados[i]} ${correto ? '✓' : '✗'}`
)
}
console.log(`\nAcurácia: ${acertos}/${inputs.length} = ${(acertos/inputs.length*100).toFixed(0)}%`)
// Deve ser 100% se o treino convergiuOutput esperado:
=== Resultado do XOR ===
[0,0] → prob: 0.0213 → pred: 0 → esperado: 0 ✓
[0,1] → prob: 0.9867 → pred: 1 → esperado: 1 ✓
[1,0] → prob: 0.9834 → pred: 1 → esperado: 1 ✓
[1,1] → prob: 0.0198 → pred: 0 → esperado: 0 ✓
Acurácia: 4/4 = 100%Note como as probabilidades são extremas (0.02 e 0.98) quando o treino converge bem. Probabilidades próximas de 0.5 indicariam incerteza — um sinal de treino insuficiente.
Padrões e Armadilhas
Padrões recomendados
Padrão 1: Monitore a curva de loss, não só o valor final Ative log: true durante desenvolvimento. A curva importa mais que o número final: uma loss que oscila indica learning rate alta; uma que planta indica que convergiu ou está presa num mínimo local.
Padrão 2: Comece com arquitetura simples e aumente gradualmente Para XOR, comece com [2] (2 neurônios ocultos). Se não convergir em 10.000 iterações, aumente para [4], depois [8]. Adicionar complexidade sem necessidade aumenta tempo de treino e risco de overfitting.
Padrão 3: Valide com dados que o modelo NÃO viu durante o treino XOR tem apenas 4 exemplos — todos usados no treino. Em problemas reais, reserve 20% dos dados para validação e teste a cada epoch. Se a loss de validação começa a subir enquanto a de treino desce, pare o treino (early stopping).
Armadilhas comuns
⚠️ Armadilha 1: Usar linear activation para classificação binária O que acontece: com activation: 'linear', a rede não aprende XOR. Funções lineares compostas são lineares — sem a não-linearidade da sigmoide, a rede equivale a um único perceptron. Versão correta: use sigmoid para saída entre 0 e 1, ou relu + sigmoide na última camada para redes mais profundas.
⚠️ Armadilha 2: Assumir que mais iterações sempre ajudam O que acontece: você roda 100.000 iterações esperando resultado melhor. Em XOR, depois de 5.000 iterações, os pesos podem estar com valores extremos (explodiram) em vez de convergidos. Versão correta: use errorThresh para parar quando o erro estiver bom o suficiente. Iterações em excesso em dataset pequeno causam overfitting severo.
⚠️ Armadilha 3: Não separar probabilidade de predição binária O que acontece: você usa saida[0] diretamente como 0 ou 1 e obtém valores como 0.73, causando erros de lógica no código downstream. Versão correta: Math.round(saida[0]) para threshold de 0.5, ou saida[0] > 0.7 para threshold personalizado. Nunca trate a probabilidade bruta como classe discreta.
Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
O starter tem 3 TODOs:
TODO 1 — Criar a instância NeuralNetwork Seção de referência: “Conceitos Fundamentais → Anatomia de uma rede neural”. Implemente: const rede = new brain.NeuralNetwork({ hiddenLayers: [4] }) Experimente com [2] e [4] para ver a diferença de convergência. Com [2], algumas execuções podem não convergir — o problema de mínimos locais.
TODO 2 — Treinar a rede Seção de referência: “Conceitos Fundamentais → Epoch e convergência” e “Aprofundamento Técnico → O papel da taxa de aprendizado”. Implemente: const resultado = rede.train(dadosTreino, { iterations: 5000, log: true, logPeriod: 500, errorThresh: 0.005 }) Observe a loss caindo a cada 500 iterações. Se não cair, a learning rate pode estar errada ou a arquitetura inadequada.
TODO 3 — Fazer inferência com rede.run() Seção de referência: “Exemplos Anotados → Exemplo 2”. Implemente: const saida = rede.run(entradas[i]). Já existe Math.round(saida[0]) na linha seguinte para converter probabilidade em predição binária.
Dica para o TODO mais difícil (TODO 1): se a rede não convergir após 10.000 iterações, o problema é a inicialização aleatória — é não-determinístico. Rode novamente. Se falhar consistentemente, aumente hiddenLayers: [4] para [8].
Agora você está pronto para o lab.