Phase 03 - Lesson 02
Redes Multicamada e Passagem Direta
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Um neurônio desenha uma reta. Empilhe vários e você consegue desenhar qualquer coisa.
Tipo: Build Linguagens: Python Pré-requisitos: Fase 01 (Fundamentos de Matemática), Lição 03.01 (O Perceptron) Tempo: ~90 minutos
Objetivos de Aprendizagem
- Construir uma rede multicamada do zero com classes Layer e Network que realizam uma passagem direta completa
- Acompanhar as dimensões das matrizes em cada camada de uma rede e identificar incompatibilidades de formato
- Explicar como empilhar ativações não lineares permite que uma rede aprenda fronteiras de decisão curvas
- Resolver o problema XOR usando uma arquitetura 2-2-1 com pesos sigmoides ajustados manualmente
O Problema
Um único neurônio é um desenhista de retas. Só isso. Uma reta atravessando seus dados. Todo problema real em IA -- reconhecimento de imagens, compreensão de linguagem, jogar Go -- exige curvas. Empilhar neurônios em camadas é como você obtém curvas.
Em 1969, Minsky e Papert provaram que essa limitação era fatal: uma rede de camada única não consegue aprender o XOR. Não é que "tem dificuldade em aprender" -- ela matematicamente não consegue. A tabela-verdade do XOR coloca [0,1] e [1,0] de um lado, e [0,0] e [1,1] do outro. Nenhuma reta única os separa.
Isso matou o financiamento de redes neurais por mais de uma década. A solução era óbvia em retrospecto: pare de usar uma só camada. Empilhe neurônios em camadas. Deixe a primeira camada esculpir o espaço de entrada em novas features e deixe a segunda camada combinar essas features em decisões que nenhuma reta única conseguiria tomar.
Essa pilha é a rede multicamada. Ela é a base de todo modelo de deep learning em produção hoje. A passagem direta -- dados fluindo da entrada, através das camadas ocultas, até a saída -- é a primeira coisa que você precisa construir antes que qualquer outra coisa funcione.
O Conceito
Camadas: Entrada, Oculta, Saída
Uma rede multicamada tem três tipos de camadas:
Camada de entrada -- não é realmente uma camada. Ela armazena seus dados brutos. Duas features significam dois nós de entrada. Nenhum cálculo acontece aqui.
Camadas ocultas -- onde o trabalho acontece. Cada neurônio recebe todas as saídas da camada anterior, aplica pesos e um viés, e então passa o resultado por uma função de ativação. "Oculta" porque você nunca vê esses valores diretamente nos dados de treinamento.
Camada de saída -- a resposta final. Para classificação binária, um neurônio com sigmoide. Para multiclasse, um neurônio por classe.
graph LR
subgraph Input["Camada de Entrada"]
x1["x1"]
x2["x2"]
end
subgraph Hidden["Camada Oculta (3 neurônios)"]
h1["h1"]
h2["h2"]
h3["h3"]
end
subgraph Output["Camada de Saída"]
y["y"]
end
x1 --> h1
x1 --> h2
x1 --> h3
x2 --> h1
x2 --> h2
x2 --> h3
h1 --> y
h2 --> y
h3 --> y
Esta é uma rede 2-3-1. Duas entradas, três neurônios ocultos, uma saída. Cada conexão carrega um peso. Cada neurônio (exceto os de entrada) carrega um viés.
Cada camada produz um vetor de números chamado estado oculto. Para texto, os estados ocultos aumentam a dimensionalidade -- codificando uma palavra como 768 números para capturar significado semântico. Para imagens, eles reduzem a dimensionalidade -- comprimindo milhões de pixels em uma representação gerenciável. O estado oculto é onde o aprendizado mora.
Neurônios e Ativações
Cada neurônio faz três coisas:
- Multiplica cada entrada pelo seu peso correspondente
- Soma todos os produtos e adiciona um viés
- Passa a soma por uma função de ativação
Por enquanto, a ativação é a sigmoide:
sigmoid(z) = 1 / (1 + e^(-z))
A sigmoide comprime qualquer número para o intervalo (0, 1). Entradas grandes e positivas empurram em direção a 1. Entradas grandes e negativas empurram em direção a 0. Zero mapeia para 0.5. Essa curva suave é o que torna o aprendizado possível -- ao contrário do degrau abrupto do perceptron, a sigmoide tem gradiente em todos os pontos.
Passagem Direta: Como os Dados Fluem
A passagem direta empurra os dados de entrada pela rede, camada por camada, até alcançar a saída. Nenhum aprendizado acontece durante a passagem direta. É puro cálculo: multiplicar, somar, ativar, repetir.
graph TD
X["Entrada: [x1, x2]"] --> WH["Multiplicar pela Matriz de Pesos W1 (2x3)"]
WH --> BH["Adicionar Vetor de Viés b1 (3,)"]
BH --> AH["Aplicar sigmoide a cada elemento"]
AH --> H["Saída Oculta: [h1, h2, h3]"]
H --> WO["Multiplicar pela Matriz de Pesos W2 (3x1)"]
WO --> BO["Adicionar Vetor de Viés b2 (1,)"]
BO --> AO["Aplicar sigmoide"]
AO --> Y["Saída: y"]
Em cada camada, três operações acontecem em sequência:
z = W * input + b (linear transformation)
a = sigmoid(z) (activation)
A saída de uma camada se torna a entrada da próxima. Essa é a passagem direta inteira.
Dimensões de Matriz
Acompanhar dimensões é a habilidade de depuração mais importante em deep learning. Aqui está a rede 2-3-1:
| Passo | Operação | Dimensões | Formato do Resultado |
|---|---|---|---|
| Entrada | x | -- | (2,) |
| Linear oculta | W1 * x + b1 | W1: (3, 2), b1: (3,) | (3,) |
| Ativação oculta | sigmoid(z1) | -- | (3,) |
| Linear de saída | W2 * h + b2 | W2: (1, 3), b2: (1,) | (1,) |
| Ativação de saída | sigmoid(z2) | -- | (1,) |
A regra: a matriz de pesos W na camada k tem formato (neurons_in_layer_k, neurons_in_layer_k_minus_1). As linhas correspondem à camada atual. As colunas correspondem à camada anterior. Se os formatos não se alinharem, você tem um bug.
Teorema da Aproximação Universal
Em 1989, George Cybenko provou algo notável: uma rede neural com uma única camada oculta e neurônios suficientes pode aproximar qualquer função contínua com qualquer precisão desejada.
Isso não significa que uma camada oculta é sempre melhor. Significa que a arquitetura é teoricamente capaz. Na prática, redes mais profundas (mais camadas, menos neurônios por camada) aprendem as mesmas funções com muito menos parâmetros totais do que redes rasas e largas. É por isso que deep learning funciona.
A intuição: cada neurônio na camada oculta aprende uma "saliência" ou feature. Saliências suficientes posicionadas nos lugares certos podem aproximar qualquer curva suave. Mais neurônios, mais saliências, melhor aproximação.
graph LR
subgraph FewNeurons["4 Neurônios Ocultos"]
A["Aproximação grosseira"]
end
subgraph MoreNeurons["16 Neurônios Ocultos"]
B["Aproximação próxima"]
end
subgraph ManyNeurons["64 Neurônios Ocultos"]
C["Ajuste quase perfeito"]
end
FewNeurons --> MoreNeurons --> ManyNeurons
Composabilidade
Redes neurais são componíveis. Você pode empilhá-las, encadeá-las, executá-las em paralelo. Um modelo Whisper usa uma rede codificadora para processar áudio e uma rede decodificadora separada para gerar texto. LLMs modernos são decoder-only. O BERT é encoder-only. O T5 é encoder-decoder. A escolha da arquitetura define o que o modelo consegue fazer.
Construa
Python puro. Sem numpy. Cada operação de matriz escrita do zero.
Passo 1: Ativação Sigmoide
import math
def sigmoid(x):
x = max(-500.0, min(500.0, x))
return 1.0 / (1.0 + math.exp(-x))
O recorte para [-500, 500] previne overflow. math.exp(500) é grande mas finito. math.exp(1000) é infinito.
Passo 2: Classe Layer
A operação mais importante em todo o deep learning é a multiplicação de matrizes. Cada camada, cada cabeça de atenção, cada passagem direta -- são matmuls até o fim. Uma camada linear recebe um vetor de entrada, multiplica-o por uma matriz de pesos e adiciona um vetor de viés: y = Wx + b. Essa única equação é 90% do cálculo em uma rede neural.
Uma camada armazena uma matriz de pesos e um vetor de viés. Seu método forward recebe um vetor de entrada e retorna a saída ativada.
class Layer:
def __init__(self, n_inputs, n_neurons, weights=None, biases=None):
if weights is not None:
self.weights = weights
else:
import random
self.weights = [
[random.uniform(-1, 1) for _ in range(n_inputs)]
for _ in range(n_neurons)
]
if biases is not None:
self.biases = biases
else:
self.biases = [0.0] * n_neurons
def forward(self, inputs):
self.last_input = inputs
self.last_output = []
for neuron_idx in range(len(self.weights)):
z = sum(
w * x for w, x in zip(self.weights[neuron_idx], inputs)
)
z += self.biases[neuron_idx]
self.last_output.append(sigmoid(z))
return self.last_output
A matriz de pesos tem formato (n_neurons, n_inputs). Cada linha são os pesos de um neurônio através de todas as entradas. O método forward percorre os neurônios, calcula a soma ponderada mais o viés, aplica a sigmoide e coleta os resultados.
Passo 3: Classe Network
Uma rede é uma lista de camadas. A passagem direta as encadeia: a saída da camada k alimenta a camada k+1.
class Network:
def __init__(self, layers):
self.layers = layers
def forward(self, inputs):
current = inputs
for layer in self.layers:
current = layer.forward(current)
return current
Essa é a passagem direta inteira. Quatro linhas de lógica. Os dados entram, fluem por todas as camadas e saem do outro lado.
Passo 4: XOR com Pesos Ajustados Manualmente
Na Lição 01, resolvemos o XOR combinando perceptrons OR, NAND e AND. Agora faça a mesma coisa com nossas classes Layer e Network. A arquitetura 2-2-1: duas entradas, dois neurônios ocultos, uma saída.
hidden = Layer(
n_inputs=2,
n_neurons=2,
weights=[[20.0, 20.0], [-20.0, -20.0]],
biases=[-10.0, 30.0],
)
output = Layer(
n_inputs=2,
n_neurons=1,
weights=[[20.0, 20.0]],
biases=[-30.0],
)
xor_net = Network([hidden, output])
xor_data = [
([0, 0], 0),
([0, 1], 1),
([1, 0], 1),
([1, 1], 0),
]
for inputs, expected in xor_data:
result = xor_net.forward(inputs)
predicted = 1 if result[0] >= 0.5 else 0
print(f" {inputs} -> {result[0]:.6f} (rounded: {predicted}, expected: {expected})")
Os pesos grandes (20, -20) fazem a sigmoide agir como uma função degrau. O primeiro neurônio oculto aproxima OR. O segundo aproxima NAND. O neurônio de saída os combina em AND, que é XOR.
Passo 5: Classificação de Círculo
Um problema mais difícil: classificar pontos 2D como dentro ou fora de um círculo de raio 0.5 centrado na origem. Isso requer uma fronteira de decisão curva -- impossível para um único perceptron.
import random
import math
random.seed(42)
data = []
for _ in range(200):
x = random.uniform(-1, 1)
y = random.uniform(-1, 1)
label = 1 if (x * x + y * y) < 0.25 else 0
data.append(([x, y], label))
circle_net = Network([
Layer(n_inputs=2, n_neurons=8),
Layer(n_inputs=8, n_neurons=1),
])
Com pesos aleatórios, a rede não classificará bem. Mas a passagem direta ainda roda. Esse é o ponto -- a passagem direta é apenas cálculo. Aprender os pesos certos é a retropropagação, que vem na Lição 03.
correct = 0
for inputs, expected in data:
result = circle_net.forward(inputs)
predicted = 1 if result[0] >= 0.5 else 0
if predicted == expected:
correct += 1
print(f"Accuracy with random weights: {correct}/{len(data)} ({100*correct/len(data):.1f}%)")
Pesos aleatórios dão acurácia ruim -- frequentemente pior do que adivinhar a classe majoritária. Após o treinamento (Lição 03), essa mesma arquitetura com 8 neurônios ocultos desenhará uma fronteira curva que separa o dentro do fora.
Use
O PyTorch faz tudo acima em quatro linhas:
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(2, 8),
nn.Sigmoid(),
nn.Linear(8, 1),
nn.Sigmoid(),
)
x = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
output = model(x)
print(output)
nn.Linear(2, 8) é a sua classe Layer: matriz de pesos de formato (8, 2), vetor de viés de formato (8,). nn.Sigmoid() é a sua função sigmoide aplicada elemento a elemento. nn.Sequential é a sua classe Network: encadeia camadas em ordem.
A diferença é velocidade e escala. O PyTorch roda em GPUs, lida com lotes de milhões de amostras e calcula automaticamente gradientes para a retropropagação. Mas a lógica da passagem direta é idêntica à que você acabou de construir do zero.
Entregue
Esta lição produz um prompt reutilizável para projetar arquiteturas de rede:
outputs/prompt-network-architect.md
Use-o quando precisar decidir quantas camadas, quantos neurônios por camada e quais funções de ativação usar para um dado problema.
Exercícios
Construa uma rede 2-4-2-1 (duas camadas ocultas) e execute a passagem direta nos dados do XOR com pesos aleatórios. Imprima as saídas intermediárias das camadas ocultas para ver como a representação se transforma em cada camada.
Mude o tamanho da camada oculta no classificador de círculo de 8 para 2, depois para 32. Execute a passagem direta com pesos aleatórios a cada vez. O número de neurônios ocultos muda o intervalo ou a distribuição da saída? Por quê?
Implemente um método
count_parametersna classe Network que retorna o número total de pesos e vieses treináveis. Teste-o em uma rede 784-256-128-10 (a clássica arquitetura do MNIST). Quantos parâmetros ela tem?Construa uma passagem direta para uma rede 3-4-4-2. Alimente-a com valores de cor RGB (normalizados para 0-1) e observe as duas saídas. Esta é a arquitetura de um classificador de cores simples com duas classes.
Substitua a sigmoide por uma função de "degrau com vazamento": retorne 0.01 * z se z < 0, caso contrário 1.0. Execute a passagem direta no XOR com os mesmos pesos ajustados manualmente do Passo 4. Ainda funciona? Por que a sigmoide suave é preferível a cortes abruptos?
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Passagem direta | "Rodar o modelo" | Empurrar a entrada por todas as camadas -- multiplicar pelos pesos, adicionar viés, ativar -- para produzir uma saída |
| Camada oculta | "A parte do meio" | Qualquer camada entre a entrada e a saída cujos valores não são diretamente observados nos dados |
| Rede multicamada | "Uma rede neural profunda" | Camadas de neurônios empilhadas sequencialmente, onde a saída de cada camada alimenta a entrada da camada seguinte |
| Função de ativação | "A não linearidade" | Uma função aplicada após a transformação linear que introduz curvas na fronteira de decisão |
| Sigmoide | "A curva em S" | sigma(z) = 1/(1+e^(-z)), comprime qualquer número real para (0,1), suave e diferenciável em todos os pontos |
| Matriz de pesos | "Os parâmetros" | Uma matriz W de formato (current_layer_neurons, previous_layer_neurons) contendo forças de conexão aprendíveis |
| Vetor de viés | "O deslocamento" | Um vetor adicionado após a multiplicação de matrizes que permite que neurônios ativem mesmo quando todas as entradas são zero |
| Aproximação universal | "Redes neurais podem aprender qualquer coisa" | Uma única camada oculta com neurônios suficientes pode aproximar qualquer função contínua -- mas "suficientes" pode significar bilhões |
| Transformação linear | "O passo da multiplicação de matrizes" | z = W * x + b, o cálculo antes da ativação, que mapeia entradas para um novo espaço |
| Fronteira de decisão | "Onde o classificador troca" | A superfície no espaço de entrada onde a saída da rede cruza o limiar de classificação |
Leitura Adicional
- Michael Nielsen, "Neural Networks and Deep Learning", Capítulos 1-2 (http://neuralnetworksanddeeplearning.com/) -- a explicação gratuita mais clara de passagens diretas e estrutura de rede, com visualizações interativas
- Cybenko, "Approximation by Superpositions of a Sigmoidal Function" (1989) -- o artigo original do teorema da aproximação universal, surpreendentemente legível
- 3Blue1Brown, "But what is a neural network?" (https://www.youtube.com/watch?v=aircAruvnKk) -- explicação visual de 20 minutos sobre camadas, pesos e passagens diretas que constrói o modelo mental certo
- Goodfellow, Bengio, Courville, "Deep Learning", Capítulo 6 (https://www.deeplearningbook.org/) -- a referência padrão para redes multicamada, gratuita online