Phase 03 - Lesson 04
Funções de Ativação
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Sem não linearidade, a sua rede de 100 camadas não passa de uma multiplicação de matrizes sofisticada. As ativações são os portões que permitem que as redes neurais pensem em curvas.
Tipo: Build Linguagens: Python Pré-requisitos: Lição 03.03 (Backpropagation) Tempo: ~75 minutos
Objetivos de Aprendizagem
- Implementar sigmoid, tanh, ReLU, Leaky ReLU, GELU, Swish e softmax com suas derivadas do zero
- Diagnosticar o problema do gradiente que desaparece medindo as magnitudes das ativações ao longo de mais de 10 camadas com diferentes ativações
- Detectar neurônios mortos em uma rede ReLU e explicar por que a GELU evita esse modo de falha
- Selecionar a função de ativação correta para uma dada arquitetura (transformer, CNN, RNN, camada de saída)
O Problema
Empilhe duas transformações lineares: y = W2(W1x + b1) + b2. Expanda: y = W2W1x + W2b1 + b2. Isso é apenas y = Ax + c -- uma única transformação linear. Não importa quantas camadas lineares você empilhe, o resultado colapsa em uma única multiplicação de matrizes. A sua rede de 100 camadas tem o mesmo poder de representação que uma única camada.
Isso não é uma curiosidade teórica. Significa que uma rede linear profunda literalmente não consegue aprender XOR, não consegue classificar um conjunto de dados em espiral, não consegue reconhecer um rosto. Sem funções de ativação, a profundidade é uma ilusão.
As funções de ativação quebram a linearidade. Elas deformam a saída de cada camada através de uma função não linear, dando à rede a capacidade de curvar fronteiras de decisão, aproximar funções arbitrárias e de fato aprender. Mas escolha a ativação errada e os seus gradientes desaparecem até zero (sigmoid em redes profundas), explodem até o infinito (ativações ilimitadas sem inicialização cuidadosa), ou os seus neurônios morrem permanentemente (ReLU com grandes vieses negativos). A escolha da função de ativação determina diretamente se a sua rede aprende ou não.
O Conceito
Por Que a Não Linearidade É Necessária
A multiplicação de matrizes é componível. Multiplicar um vetor pela matriz A e depois pela matriz B é idêntico a multiplicar por AB. Isso significa que empilhar dez camadas lineares é matematicamente equivalente a uma única camada linear com uma matriz grande. Todos esses parâmetros, toda essa profundidade -- desperdiçados. Você precisa de algo para quebrar a cadeia. É isso que as funções de ativação fazem.
Aqui está a prova. Uma camada linear computa f(x) = Wx + b. Empilhe duas:
Layer 1: h = W1 * x + b1
Layer 2: y = W2 * h + b2
Substitua:
y = W2 * (W1 * x + b1) + b2
y = (W2 * W1) * x + (W2 * b1 + b2)
y = A * x + c
Uma camada. Insira uma ativação não linear g() entre as camadas:
h = g(W1 * x + b1)
y = W2 * h + b2
Agora a substituição quebra. W2 * g(W1 * x + b1) + b2 não pode ser reduzido a uma única transformação linear. A rede consegue representar funções não lineares. Cada camada adicional com uma ativação acrescenta capacidade de representação.
Sigmoid
A função de ativação original para redes neurais.
sigmoid(x) = 1 / (1 + e^(-x))
Faixa de saída: (0, 1). Suave, diferenciável, mapeia qualquer número real para um valor semelhante a uma probabilidade.
A derivada:
sigmoid'(x) = sigmoid(x) * (1 - sigmoid(x))
O valor máximo dessa derivada é 0,25, ocorrendo em x = 0. Na backpropagation, os gradientes se multiplicam ao longo das camadas. Dez camadas de sigmoid significam que o gradiente é multiplicado por no máximo 0,25 dez vezes:
0.25^10 = 0.000000953674
Menos de um milionésimo do sinal original. Esse é o problema do gradiente que desaparece. Os gradientes nas primeiras camadas ficam tão pequenos que os pesos quase não se atualizam. A rede parece aprender -- a perda diminui nas camadas posteriores -- mas as primeiras camadas estão congeladas. Redes sigmoid profundas simplesmente não treinam.
Problema adicional: as saídas da sigmoid são sempre positivas (0 a 1), o que significa que os gradientes nos pesos têm sempre o mesmo sinal. Isso causa um zigue-zague durante o gradiente descendente.
Tanh
A versão centralizada da sigmoid.
tanh(x) = (e^x - e^(-x)) / (e^x + e^(-x))
Faixa de saída: (-1, 1). Centrada em zero, o que elimina o problema do zigue-zague.
A derivada:
tanh'(x) = 1 - tanh(x)^2
A derivada máxima é 1,0 em x = 0 -- quatro vezes melhor que a sigmoid. Mas o problema do gradiente que desaparece ainda existe. Para entradas positivas ou negativas grandes, a derivada se aproxima de zero. Dez camadas ainda esmagam o gradiente, apenas de forma menos agressiva.
ReLU: O Avanço
Rectified Linear Unit (Unidade Linear Retificada). Popularizada para deep learning por Nair e Hinton em 2010 (a função em si remonta ao trabalho de Fukushima de 1969), ela mudou tudo.
relu(x) = max(0, x)
Faixa de saída: [0, infinito). A derivada é trivialmente simples:
relu'(x) = 1 if x > 0
0 if x <= 0
Sem gradiente que desaparece para entradas positivas. O gradiente é exatamente 1, repassado diretamente. É por isso que as redes profundas se tornaram treináveis -- a ReLU preserva a magnitude do gradiente ao longo das camadas.
Mas existe um modo de falha: o problema do neurônio morto. Se a entrada ponderada de um neurônio é sempre negativa (devido a um grande viés negativo ou a uma inicialização infeliz dos pesos), sua saída é sempre zero, seu gradiente é sempre zero e ele nunca se atualiza. Ele está permanentemente morto. Na prática, de 10 a 40% dos neurônios em uma rede ReLU podem morrer durante o treinamento.
Leaky ReLU
A correção mais simples para neurônios mortos.
leaky_relu(x) = x if x > 0
alpha * x if x <= 0
Onde alpha é uma constante pequena, tipicamente 0,01. O lado negativo tem uma pequena inclinação em vez de zero, então os neurônios mortos ainda recebem um sinal de gradiente e podem se recuperar.
GELU: O Padrão Moderno
Gaussian Error Linear Unit (Unidade Linear de Erro Gaussiano). Introduzida por Hendrycks e Gimpel em 2016. Ativação padrão no BERT, GPT e na maioria dos transformers modernos.
gelu(x) = x * Phi(x)
Onde Phi(x) é a função de distribuição cumulativa da distribuição normal padrão. A aproximação usada na prática:
gelu(x) ~= 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))
A GELU é suave em todos os pontos, permite pequenos valores negativos (ao contrário da ReLU, que recorta de forma abrupta para zero) e tem uma interpretação probabilística: ela pondera cada entrada pela probabilidade de ela ser positiva sob uma distribuição gaussiana. Esse portão suave supera a ReLU em arquiteturas transformer porque proporciona um melhor fluxo de gradiente e evita completamente o problema do neurônio morto.
Swish / SiLU
Ativação auto-portada descoberta por Ramachandran et al. em 2017 por meio de busca automatizada.
swish(x) = x * sigmoid(x)
A Swish é formalmente x * sigmoid(x). O Google a descobriu por meio de busca automatizada no espaço de funções de ativação -- uma rede neural projetando partes de redes neurais.
Como a GELU, ela é suave, não monotônica e permite pequenos valores negativos. A diferença é sutil: a Swish usa a sigmoid para o portão enquanto a GELU usa a CDF gaussiana. Na prática, o desempenho é quase idêntico. A Swish é usada na EfficientNet e em alguns modelos de visão. A GELU domina nos modelos de linguagem.
Softmax: A Ativação de Saída
Não usada em camadas ocultas. A softmax converte um vetor de pontuações brutas (logits) em uma distribuição de probabilidade.
softmax(x_i) = e^(x_i) / sum(e^(x_j) for all j)
Cada saída fica entre 0 e 1. Todas as saídas somam 1. Isso a torna a ativação final padrão para classificação multiclasse. O maior logit recebe a maior probabilidade, mas, ao contrário de argmax, a softmax é diferenciável e preserva informação sobre a confiança relativa.
Comparação de Formatos
graph LR
subgraph "Funções de Ativação"
S["Sigmoid<br/>Faixa: (0,1)<br/>Satura em ambas as pontas"]
T["Tanh<br/>Faixa: (-1,1)<br/>Centrada em zero"]
R["ReLU<br/>Faixa: [0,inf)<br/>Neurônios mortos"]
G["GELU<br/>Faixa: ~(-0.17,inf)<br/>Portão suave"]
end
S -->|"Gradiente que desaparece"| Problem["Redes profundas<br/>não treinam"]
T -->|"Menos severo, mas<br/>ainda desaparece"| Problem
R -->|"Gradiente = 1<br/>para x > 0"| Solution["Redes profundas<br/>treinam rápido"]
G -->|"Gradiente suave<br/>em todo lugar"| Solution
Comparação de Fluxo de Gradiente
graph TD
Input["Sinal de Entrada"] --> L1["Camada 1"]
L1 --> L5["Camada 5"]
L5 --> L10["Camada 10"]
L10 --> Output["Saída"]
subgraph "Gradiente na Camada 1"
SigGrad["Sigmoid: ~0.000001"]
TanhGrad["Tanh: ~0.001"]
ReluGrad["ReLU: ~1.0"]
GeluGrad["GELU: ~0.8"]
end
Qual Ativação Quando
flowchart TD
Start["O que você está construindo?"] --> Hidden{"Camadas ocultas<br/>ou saída?"}
Hidden -->|"Camadas ocultas"| Arch{"Arquitetura?"}
Hidden -->|"Camada de saída"| Task{"Tipo de tarefa?"}
Arch -->|"Transformer / NLP"| GELU["Use GELU"]
Arch -->|"CNN / Visão"| ReLU["Use ReLU ou Swish"]
Arch -->|"RNN / LSTM"| Tanh["Use Tanh"]
Arch -->|"MLP simples"| ReLU2["Use ReLU"]
Task -->|"Classificação binária"| Sigmoid["Use Sigmoid"]
Task -->|"Classificação multiclasse"| Softmax["Use Softmax"]
Task -->|"Regressão"| Linear["Use Linear (sem ativação)"]
Construa
Passo 1: Implementar Todas as Funções de Ativação com Derivadas
Cada função recebe um único float e retorna um float. Cada função de derivada recebe a mesma entrada e retorna o gradiente.
import math
def sigmoid(x):
x = max(-500, min(500, x))
return 1.0 / (1.0 + math.exp(-x))
def sigmoid_derivative(x):
s = sigmoid(x)
return s * (1 - s)
def tanh_act(x):
return math.tanh(x)
def tanh_derivative(x):
t = math.tanh(x)
return 1 - t * t
def relu(x):
return max(0.0, x)
def relu_derivative(x):
return 1.0 if x > 0 else 0.0
def leaky_relu(x, alpha=0.01):
return x if x > 0 else alpha * x
def leaky_relu_derivative(x, alpha=0.01):
return 1.0 if x > 0 else alpha
def gelu(x):
return 0.5 * x * (1 + math.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * x ** 3)))
def gelu_derivative(x):
phi = 0.5 * (1 + math.erf(x / math.sqrt(2)))
pdf = math.exp(-0.5 * x * x) / math.sqrt(2 * math.pi)
return phi + x * pdf
def swish(x):
return x * sigmoid(x)
def swish_derivative(x):
s = sigmoid(x)
return s + x * s * (1 - s)
def softmax(xs):
max_x = max(xs)
exps = [math.exp(x - max_x) for x in xs]
total = sum(exps)
return [e / total for e in exps]
Passo 2: Visualizar Onde os Gradientes Morrem
Compute o gradiente em 100 pontos uniformemente espaçados de -5 a 5. Imprima um histograma de texto mostrando onde o gradiente de cada ativação fica próximo de zero.
def gradient_scan(name, derivative_fn, start=-5, end=5, n=100):
step = (end - start) / n
near_zero = 0
healthy = 0
for i in range(n):
x = start + i * step
g = derivative_fn(x)
if abs(g) < 0.01:
near_zero += 1
else:
healthy += 1
pct_dead = near_zero / n * 100
print(f"{name:15s}: {healthy:3d} healthy, {near_zero:3d} near-zero ({pct_dead:.0f}% dead zone)")
gradient_scan("Sigmoid", sigmoid_derivative)
gradient_scan("Tanh", tanh_derivative)
gradient_scan("ReLU", relu_derivative)
gradient_scan("Leaky ReLU", leaky_relu_derivative)
gradient_scan("GELU", gelu_derivative)
gradient_scan("Swish", swish_derivative)
Passo 3: Experimento do Gradiente que Desaparece
Faça a passagem direta (forward pass) de um sinal através de N camadas usando sigmoid vs ReLU. Meça como a magnitude da ativação muda.
import random
def vanishing_gradient_experiment(activation_fn, name, n_layers=10, n_inputs=5):
random.seed(42)
values = [random.gauss(0, 1) for _ in range(n_inputs)]
print(f"\n{name} through {n_layers} layers:")
for layer in range(n_layers):
weights = [random.gauss(0, 1) for _ in range(n_inputs)]
z = sum(w * v for w, v in zip(weights, values))
activated = activation_fn(z)
magnitude = abs(activated)
bar = "#" * int(magnitude * 20)
print(f" Layer {layer+1:2d}: magnitude = {magnitude:.6f} {bar}")
values = [activated] * n_inputs
vanishing_gradient_experiment(sigmoid, "Sigmoid")
vanishing_gradient_experiment(relu, "ReLU")
vanishing_gradient_experiment(gelu, "GELU")
Passo 4: Detector de Neurônios Mortos
Crie uma rede ReLU, passe entradas aleatórias por ela e conte quantos neurônios nunca disparam.
def dead_neuron_detector(n_inputs=5, hidden_size=20, n_samples=1000):
random.seed(0)
weights = [[random.gauss(0, 1) for _ in range(n_inputs)] for _ in range(hidden_size)]
biases = [random.gauss(0, 1) for _ in range(hidden_size)]
fire_counts = [0] * hidden_size
for _ in range(n_samples):
inputs = [random.gauss(0, 1) for _ in range(n_inputs)]
for neuron_idx in range(hidden_size):
z = sum(w * x for w, x in zip(weights[neuron_idx], inputs)) + biases[neuron_idx]
if relu(z) > 0:
fire_counts[neuron_idx] += 1
dead = sum(1 for c in fire_counts if c == 0)
rarely_fire = sum(1 for c in fire_counts if 0 < c < n_samples * 0.05)
healthy = hidden_size - dead - rarely_fire
print(f"\nDead Neuron Report ({hidden_size} neurons, {n_samples} samples):")
print(f" Dead (never fired): {dead}")
print(f" Barely alive (<5%): {rarely_fire}")
print(f" Healthy: {healthy}")
print(f" Dead neuron rate: {dead/hidden_size*100:.1f}%")
for i, c in enumerate(fire_counts):
status = "DEAD" if c == 0 else "WEAK" if c < n_samples * 0.05 else "OK"
bar = "#" * (c * 40 // n_samples)
print(f" Neuron {i:2d}: {c:4d}/{n_samples} fires [{status:4s}] {bar}")
dead_neuron_detector()
Passo 5: Comparação de Treinamento -- Sigmoid vs ReLU vs GELU
Treine a mesma rede de duas camadas no conjunto de dados do círculo (pontos dentro de um círculo = classe 1, fora = classe 0) com três ativações diferentes. Compare a velocidade de convergência.
def make_circle_data(n=200, seed=42):
random.seed(seed)
data = []
for _ in range(n):
x = random.uniform(-2, 2)
y = random.uniform(-2, 2)
label = 1.0 if x * x + y * y < 1.5 else 0.0
data.append(([x, y], label))
return data
class ActivationNetwork:
def __init__(self, activation_fn, activation_deriv, hidden_size=8, lr=0.1):
random.seed(0)
self.act = activation_fn
self.act_d = activation_deriv
self.lr = lr
self.hidden_size = hidden_size
self.w1 = [[random.gauss(0, 0.5) for _ in range(2)] for _ in range(hidden_size)]
self.b1 = [0.0] * hidden_size
self.w2 = [random.gauss(0, 0.5) for _ in range(hidden_size)]
self.b2 = 0.0
def forward(self, x):
self.x = x
self.z1 = []
self.h = []
for i in range(self.hidden_size):
z = self.w1[i][0] * x[0] + self.w1[i][1] * x[1] + self.b1[i]
self.z1.append(z)
self.h.append(self.act(z))
self.z2 = sum(self.w2[i] * self.h[i] for i in range(self.hidden_size)) + self.b2
self.out = sigmoid(self.z2)
return self.out
def backward(self, target):
error = self.out - target
d_out = error * self.out * (1 - self.out)
for i in range(self.hidden_size):
d_h = d_out * self.w2[i] * self.act_d(self.z1[i])
self.w2[i] -= self.lr * d_out * self.h[i]
for j in range(2):
self.w1[i][j] -= self.lr * d_h * self.x[j]
self.b1[i] -= self.lr * d_h
self.b2 -= self.lr * d_out
def train(self, data, epochs=200):
losses = []
for epoch in range(epochs):
total_loss = 0
correct = 0
for x, y in data:
pred = self.forward(x)
self.backward(y)
total_loss += (pred - y) ** 2
if (pred >= 0.5) == (y >= 0.5):
correct += 1
avg_loss = total_loss / len(data)
accuracy = correct / len(data) * 100
losses.append(avg_loss)
if epoch % 50 == 0 or epoch == epochs - 1:
print(f" Epoch {epoch:3d}: loss={avg_loss:.4f}, accuracy={accuracy:.1f}%")
return losses
data = make_circle_data()
configs = [
("Sigmoid", sigmoid, sigmoid_derivative),
("ReLU", relu, relu_derivative),
("GELU", gelu, gelu_derivative),
]
results = {}
for name, act_fn, act_d_fn in configs:
print(f"\n=== Training with {name} ===")
net = ActivationNetwork(act_fn, act_d_fn, hidden_size=8, lr=0.1)
losses = net.train(data, epochs=200)
results[name] = losses
print("\n=== Final Loss Comparison ===")
for name, losses in results.items():
print(f" {name:10s}: start={losses[0]:.4f} -> end={losses[-1]:.4f} (improvement: {(1 - losses[-1]/losses[0])*100:.1f}%)")
Use
O PyTorch fornece todas elas tanto na forma funcional quanto na forma de módulo:
import torch
import torch.nn as nn
import torch.nn.functional as F
x = torch.randn(4, 10)
relu_out = F.relu(x)
gelu_out = F.gelu(x)
sigmoid_out = torch.sigmoid(x)
swish_out = F.silu(x)
logits = torch.randn(4, 5)
probs = F.softmax(logits, dim=1)
model = nn.Sequential(
nn.Linear(10, 64),
nn.GELU(),
nn.Linear(64, 32),
nn.GELU(),
nn.Linear(32, 5),
)
Camadas ocultas em um transformer: GELU. Camadas ocultas em uma CNN: ReLU. Camada de saída para classificação: softmax. Camada de saída para regressão: nenhuma (linear). Camada de saída para probabilidades: sigmoid. É isso. Comece com esses padrões. Mude-os apenas quando tiver evidências.
RNNs e LSTMs usam tanh para o estado oculto e sigmoid para os portões, mas se você está construindo do zero hoje, provavelmente não está usando RNNs. Se os neurônios estão morrendo na sua rede ReLU, troque para GELU. Não recorra à Leaky ReLU a menos que tenha um motivo específico -- a GELU resolve o problema do neurônio morto e dá um melhor fluxo de gradiente.
Entregue
Esta lição produz:
outputs/prompt-activation-selector.md-- um prompt reutilizável que ajuda você a escolher a função de ativação certa para qualquer arquitetura
Exercícios
Implemente a Parametric ReLU (PReLU), onde a inclinação negativa alpha é um parâmetro aprendível. Treine-a no conjunto de dados do círculo e compare com a Leaky ReLU fixa.
Rode o experimento do gradiente que desaparece com 50 camadas em vez de 10. Plote a magnitude em cada camada para sigmoid, tanh, ReLU e GELU. Em qual camada o sinal de cada ativação efetivamente chega a zero?
Implemente a ELU (Exponential Linear Unit): elu(x) = x se x > 0, alpha * (e^x - 1) se x <= 0. Compare sua taxa de neurônios mortos com a da ReLU na mesma rede.
Construa um "monitor de saúde de gradiente" que rode durante o treinamento: a cada época, compute a magnitude média do gradiente em cada camada. Imprima um aviso quando o gradiente de qualquer camada cair abaixo de 0,001 ou exceder 100.
Modifique a comparação de treinamento para usar o conjunto de dados XOR da Lição 01 em vez de círculos. Qual ativação converge mais rápido no XOR? Por que isso difere dos resultados do círculo?
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Função de ativação | "A parte não linear" | Uma função aplicada à saída de cada neurônio que quebra a linearidade, permitindo que a rede aprenda mapeamentos não lineares |
| Gradiente que desaparece | "Os gradientes somem em redes profundas" | Os gradientes encolhem exponencialmente ao longo das camadas quando a derivada da ativação é menor que 1, tornando as primeiras camadas não treináveis |
| Gradiente que explode | "Os gradientes estouram" | Os gradientes crescem exponencialmente ao longo das camadas quando o multiplicador efetivo excede 1, causando treinamento instável |
| Neurônio morto | "Um neurônio que parou de aprender" | Um neurônio ReLU cuja entrada é permanentemente negativa, produzindo saída zero e gradiente zero |
| Sigmoid | "Comprime valores para 0-1" | A função logística 1/(1+e^-x), historicamente importante mas que causa gradientes que desaparecem em redes profundas |
| ReLU | "Recorta negativos para zero" | max(0, x) -- a ativação que tornou o deep learning prático ao preservar a magnitude do gradiente |
| GELU | "A ativação do transformer" | Gaussian Error Linear Unit, uma ativação suave que pondera as entradas pela probabilidade de serem positivas |
| Swish/SiLU | "ReLU auto-portada" | x * sigmoid(x), descoberta por busca automatizada, usada na EfficientNet |
| Softmax | "Transforma pontuações em probabilidades" | Normaliza um vetor de logits em uma distribuição de probabilidade onde todos os valores estão em (0,1) e somam 1 |
| Leaky ReLU | "ReLU que não morre" | max(alpha*x, x) onde alpha é pequeno (0,01), prevenindo neurônios mortos ao permitir pequenos gradientes negativos |
| Saturação | "A parte plana da sigmoid" | Regiões onde a derivada de uma ativação se aproxima de zero, bloqueando o fluxo de gradiente |
| Logit | "A pontuação bruta antes da softmax" | A saída não normalizada da camada final antes de aplicar softmax ou sigmoid |
Leituras Adicionais
- Nair & Hinton, "Rectified Linear Units Improve Restricted Boltzmann Machines" (2010) -- o artigo que introduziu a ReLU e permitiu o treinamento de redes profundas
- Hendrycks & Gimpel, "Gaussian Error Linear Units (GELUs)" (2016) -- introduziu a função de ativação que se tornou o padrão para transformers
- Ramachandran et al., "Searching for Activation Functions" (2017) -- usou busca automatizada para descobrir a Swish, mostrando que o design de ativações pode ser automatizado
- Glorot & Bengio, "Understanding the difficulty of training deep feedforward neural networks" (2010) -- o artigo que diagnosticou os gradientes que desaparecem/explodem e propôs a inicialização Xavier
- Goodfellow, Bengio, Courville, "Deep Learning" Capítulo 6.3 (https://www.deeplearningbook.org/) -- tratamento rigoroso de unidades ocultas e funções de ativação