Nos posts anteriores, construímos a base do MLP: entendemos sua estrutura e feedforward e como ele aprende através do backpropagation. Agora vamos levar esse conhecimento para problemas mais realistas.
Outras formas de medir o erro
Até agora, utilizamos o MAE (Mean Absolute Error, ou Erro Médio Absoluto) para monitorar a rede. Ele é excelente pela simplicidade: apenas nos diz a distância média entre a previsão e a realidade. No entanto, monitorar o erro e treinar a rede não precisam usar exatamente a mesma medida. Dependendo do problema, outras funções de perda podem gerar gradientes mais úteis para o aprendizado.
MAE: Mean Absolute Error (Erro Médio Absoluto)
$$ MAE = \frac{1}{n} \sum |target - prediction| $$
É a média das diferenças absolutas. É útil para interpretar a distância média entre previsão e alvo, sem dar peso extra a erros grandes.
mae = np.mean(np.abs(expected - predicted))
MSE: Mean Squared Error (Erro Quadrático Médio)
$$ MSE = \frac{1}{n} \sum (target - prediction)^2 $$
O MSE eleva ao quadrado antes de tirar a média. Ao elevar ao quadrado, erros grandes tornam-se proporcionalmente muito maiores que erros pequenos, forçando a rede a dar uma atenção muito maior para corrigir os erros mais grosseiros, pressionando o aprendizado a ser mais preciso onde a falha é maior.
mse = np.mean(np.power(expected - predicted, 2))
RMSE: Root Mean Squared Error (Raiz do Erro Quadrático Médio)
$$ RMSE = \sqrt{\frac{1}{n} \sum (target - prediction)^2} $$
É simplesmente a raiz quadrada do MSE. Como o MSE entrega um valor “ao quadrado”, o que pode ser difícil de interpretar, o RMSE traz esse valor de volta para a mesma escala dos dados originais, mantendo a característica de ser bem mais rigoroso com erros grandes do que o MAE.
rmse = np.sqrt(np.mean(np.power(expected - predicted, 2)))
Outras abordagens
- Binary Cross-Entropy: muito usada em problemas de “sim ou não” (como o XOR), pois mede o erro com base em probabilidades.
- Huber Loss: um meio-termo entre MAE e MSE. Para erros pequenos, ela se comporta de forma parecida com o MSE; para erros grandes, fica mais próxima do MAE, reduzindo o impacto de pontos fora do padrão.
A escolha da função de erro define o caráter do aprendizado da rede neural: se ela será mais tolerante a pequenos deslizes ou se será rígida com qualquer erro que fuja do padrão.
Aplicações Práticas no Mundo Real
Note que os conjuntos de dados abaixo são fictícios e simplificados. Eles servem para mostrar o processo de aprendizado, não para tomar decisões reais de crédito ou saúde.
Prática: Análise de Risco de Crédito
Neste cenário, a rede analisa três variáveis de um cliente: Salário, Dívida e Histórico de Crédito. O objetivo é prever, de forma didática, a probabilidade de aprovação de um empréstimo. Como os dados reais podem ter escalas diferentes, utilizamos valores normalizados (entre 0 e 1) para facilitar a convergência.
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return x * (1 - x)
# Dataset: [Salário, Dívida, Histórico]
# 1.0 = Alto/Excelente, 0.0 = Baixo/Ruim
inputs = np.array([
[0.8, 0.1, 1.0], # Salário alto, dívida baixa, ótimo histórico -> Aprovar (1)
[0.2, 0.8, 0.1], # Salário baixo, dívida alta, histórico ruim -> Negar (0)
[0.5, 0.5, 0.5], # Médio em tudo -> Risco (0)
[0.7, 0.6, 0.9], # Salário alto, mas dívida alta e bom histórico -> Aprovar (1)
])
expected_outputs = np.array([[1], [0], [0], [1]])
# Inicialização (3 entradas -> 4 ocultos -> 1 saída) com Bias
np.random.seed(42)
weights_0 = np.random.uniform(size=(3, 4))
weights_1 = np.random.uniform(size=(4, 1))
bias_0 = np.random.uniform(size=(1, 4))
bias_1 = np.random.uniform(size=(1, 1))
epochs = 100_000
learning_rate = 0.2
for epoch in range(epochs):
# Forward Pass com Bias
layer_1 = sigmoid(np.dot(inputs, weights_0) + bias_0)
layer_2 = sigmoid(np.dot(layer_1, weights_1) + bias_1)
# Backpropagation
error = expected_outputs - layer_2
d_layer_2 = error * sigmoid_derivative(layer_2)
d_layer_1 = np.dot(d_layer_2, weights_1.T) * sigmoid_derivative(layer_1)
# Atualização de Pesos e Bias
weights_1 += np.dot(layer_1.T, d_layer_2) * learning_rate
bias_1 += np.sum(d_layer_2, axis=0) * learning_rate
weights_0 += np.dot(inputs.T, d_layer_1) * learning_rate
bias_0 += np.sum(d_layer_1, axis=0) * learning_rate
# Teste: Cliente com Salário Alto (0.9), Dívida Média (0.4) e Histórico Bom (0.7)
novo_cliente = np.array([[0.9, 0.4, 0.7]])
l1 = sigmoid(np.dot(novo_cliente, weights_0) + bias_0)
resultado = sigmoid(np.dot(l1, weights_1) + bias_1)
print(f"Probabilidade de Aprovação: {resultado[0][0]:.4f}")
Prática: Triagem Médica - Risco de Diabetes
Aqui, a rede processa quatro indicadores fictícios de saúde: Glicose, IMC, Pressão Arterial e Idade. A camada oculta permite que a rede represente correlações entre esses fatores, mas este exemplo não deve ser interpretado como triagem médica real.
import numpy as np
np.random.seed(42)
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return x * (1 - x)
# Dataset: [Glicose, IMC, Pressão, Idade]
inputs = np.array([
[0.9, 0.8, 0.7, 0.6], # Todos os indicadores altos -> Alto Risco (1)
[0.2, 0.3, 0.4, 0.2], # Indicadores baixos -> Baixo Risco (0)
[0.6, 0.9, 0.5, 0.4], # Glicose média mas IMC muito alto -> Alto Risco (1)
[0.4, 0.2, 0.3, 0.8], # Idoso com bons indicadores -> Baixo Risco (0)
])
expected_outputs = np.array([[1], [0], [1], [0]])
# Arquitetura (4 entradas -> 5 ocultos -> 1 saída) com Bias
weights_in = np.random.uniform(size=(4, 5))
weights_out = np.random.uniform(size=(5, 1))
b_hidden = np.random.uniform(size=(1, 5))
b_out = np.random.uniform(size=(1, 1))
learning_rate = 0.1
for epoch in range(200_000):
l1 = sigmoid(np.dot(inputs, weights_in) + b_hidden)
l2 = sigmoid(np.dot(l1, weights_out) + b_out)
err = expected_outputs - l2
d_l2 = err * sigmoid_derivative(l2)
d_l1 = np.dot(d_l2, weights_out.T) * sigmoid_derivative(l1)
weights_out += np.dot(l1.T, d_l2) * learning_rate
b_out += np.sum(d_l2, axis=0) * learning_rate
weights_in += np.dot(inputs.T, d_l1) * learning_rate
b_hidden += np.sum(d_l1, axis=0) * learning_rate
# Teste: Paciente com Glicose Alta (0.85) e outros indicadores normais (0.5)
paciente = np.array([[0.85, 0.5, 0.5, 0.5]])
res = sigmoid(np.dot(sigmoid(np.dot(paciente, weights_in) + b_hidden), weights_out) + b_out)
print(f"Risco de Diabetes: {'Alto' if res > 0.5 else 'Baixo'} ({res[0][0]:.4f})")
Design de Arquitetura
Após dominar o ciclo de aprendizado, surgem as dúvidas de projeto: como estruturar a rede para que ela funcione bem em problemas reais? Esta seção reúne as decisões mais importantes nesse processo.
Redes Neurais com Múltiplas Saídas
Até agora, focamos em redes que entregam apenas um número na saída. Mas, e se precisarmos que a rede identifique vários atributos ao mesmo tempo, como “tem gato”, “tem cachorro” e “tem pássaro” em uma imagem? Para isso, utilizamos redes com múltiplos neurônios na camada de saída.
A principal mudança está nas dimensões das matrizes e pesos. Se a nossa camada escondida tem 3 neurônios e precisamos de 2 saídas, a matriz de pesos final terá a dimensão 3x2. Cada neurônio de saída terá seu próprio conjunto de pesos para receber o que a camada escondida processou.
Em redes com múltiplas saídas, o erro deixa de ser um único número e passa a ser um vetor — se temos 2 saídas, teremos 2 erros. No backpropagation, cada um desses erros gera o seu próprio Delta ($\delta$), que viaja de volta para ajustar os pesos que chegam especificamente naquele neurônio.
Quando as classes são mutuamente exclusivas, como “gato ou cachorro ou pássaro”, é comum usar uma ativação como softmax na saída. Para manter o foco no MLP, o exemplo abaixo continua usando Sigmoide.
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# Pesos: Oculta (3 neurônios) -> Saída (2 neurônios) — note a dimensão (3, 2)
weights_hidden_output = np.array([
[0.1, 0.5],
[0.8, -0.2],
[0.4, 0.1]
])
# Simulação da ativação da camada oculta (3 neurônios)
hidden_layer_output = np.array([[0.6, 0.1, 0.9]])
# O resultado será um vetor com 2 previsões
predictions = sigmoid(np.dot(hidden_layer_output, weights_hidden_output))
print(f"Previsões (2 neurônios de saída): {predictions}")
# Saída esperada: algo como [[0.74, 0.59]]
Quantas camadas e neurônios usar?
Uma das dúvidas mais comuns ao projetar uma rede neural é: como definir o número ideal de camadas e neurônios? Embora não exista uma fórmula matemática exata para isso, existem diretrizes técnicas baseadas em experimentação (heurísticas).
- Uma camada oculta: pode ser suficiente para muitos problemas simples. De acordo com o Teorema da Aproximação Universal, uma única camada oculta com neurônios suficientes pode aproximar funções contínuas sob certas condições.
- Múltiplas camadas: são indicadas quando queremos aprender representações em diferentes níveis de abstração, como em imagens, áudio ou texto.
O segredo é encontrar o equilíbrio: neurônios de menos impedem a rede de aprender (underfitting), enquanto neurônios demais fazem a rede decorar os dados e perder a capacidade de generalizar (overfitting). Comece com uma arquitetura simples e aumente a complexidade gradualmente conforme monitora o comportamento do erro.
Validação Cruzada (Cross-Validation)
Muitas vezes, uma rede neural pode apresentar um erro baixo apenas porque “deu sorte” na divisão entre dados de treino e teste. A Validação Cruzada resolve isso dividindo os dados em várias partes (K-folds).
O modelo é treinado e testado múltiplas vezes, alternando qual parte serve de teste. O resultado final é a média desses testes, garantindo que o modelo realmente aprendeu o padrão geral e não apenas decorou um subconjunto específico dos dados.
AutoML (Automated Machine Learning)
Como vimos, escolher o número de camadas e neurônios pode ser um processo exaustivo de tentativa e erro. O AutoML é uma abordagem que visa automatizar essa busca.
Através de algoritmos especializados, o sistema testa automaticamente diversas combinações de arquiteturas, taxas de aprendizagem e outros parâmetros para encontrar o modelo mais eficiente, poupando centenas de horas de ajuste manual.
O Problema do Gradiente Sumindo (Vanishing Gradient)
Neste ponto, vale mencionar um dos maiores obstáculos históricos no treinamento de redes neurais profundas: o vanishing gradient problem.
Vimos que no backpropagation o erro viaja da saída para a entrada através de multiplicações sucessivas. Como a derivada da Sigmoide tem um valor máximo de apenas 0,25, quando empilhamos muitas camadas ocultas, acabamos multiplicando esses valores decimais repetidamente ($0,25 \cdot 0,25 \cdot 0,25 \dots$).
O resultado é que o gradiente fica tão pequeno que, quando chega às primeiras camadas da rede, o ajuste nos pesos é praticamente zero, fazendo com que as camadas iniciais parem de aprender, efetivamente travando o desenvolvimento da rede.
Este problema foi um dos motivos pelos quais redes neurais profundas foram difíceis de treinar por anos. Hoje, ele é reduzido com o uso de outras funções de ativação, como a ReLU (que não satura em valores positivos), e técnicas de inicialização de pesos mais avançadas.
Além das MLPs: O Ecossistema das Redes Neurais
As Redes Neurais Multicamada ajudam a formar a base conceitual de arquiteturas muito mais complexas. Embora as MLPs sejam poderosas, elas tratam as entradas como vetores sem explorar diretamente estrutura espacial ou temporal. Para problemas específicos, surgiram variações especializadas que hoje sustentam boa parte da IA moderna:
Redes Neurais Convolucionais (CNN)
Especializadas em dados com estrutura de grade, como imagens. As CNNs utilizam filtros que deslizam pela imagem para identificar padrões espaciais, como bordas, texturas e objetos, em vez de ligar cada pixel a um neurônio de forma totalmente independente. Elas aparecem em áreas como reconhecimento facial, visão computacional e direção assistida.
Redes Neurais Recorrentes (RNN)
Projetadas para lidar com sequências e dados temporais. As RNNs possuem loops internos que permitem que a informação persista, diferente das MLPs. Elas têm uma espécie de “memória” do que aconteceu no passo anterior, sendo úteis em tarefas como processamento de voz, texto e séries temporais.
Transformers
A arquitetura que permitiu o surgimento dos modernos modelos de linguagem (LLMs). Os Transformers utilizam um mecanismo chamado Atenção, que permite que a rede foque nas partes mais importantes de uma sequência de dados, independentemente da distância entre elas.
Eles substituíram as RNNs em muitas tarefas de texto por serem mais paralelizáveis e escaláveis. É a base de tecnologias como o ChatGPT.
Redes Generativas (GANs)
Redes que não apenas classificam, mas criam novos dados. No caso das GANs (Generative Adversarial Networks), duas redes competem entre si: uma tenta criar dados falsos e a outra tenta detectar a fraude. Esse duelo leva à criação de conteúdos extremamente realistas, como as imagens geradas pelo Midjourney ou DALL-E.
Conclusão
A transição do Perceptron simples para as Redes Multicamadas atacou o problema da não-linearidade: o segredo para processar esses problemas não está apenas no aumento do número de neurônios, mas na forma como as camadas escondidas aprendem representações intermediárias antes da decisão final.
Através do uso de funções deriváveis como a Sigmoide e da aplicação do backpropagation, transformamos o aprendizado em um processo contínuo de minimização de erro. Conceitos que vimos aqui, como o bias para flexibilidade de ativação e a Descida do Gradiente para ajuste dos pesos, continuam presentes no núcleo das arquiteturas de deep learning mais avançadas, mostrando que a base matemática estabelecida há décadas ainda sustenta boa parte da inteligência artificial moderna.
Com o entendimento desses fundamentos, estamos prontos para explorar como essas redes podem ser otimizadas para processar volumes massivos de informação e lidar com desafios ainda mais profundos.
Espero que esta série tenha ajudado a entender um pouco do funcionamento das Redes Neurais Multicamadas.
Até a próxima!