Функции активации: почему ReLU и GELU решают всё
opensourceaillmactivation-functionsdeep-learningit
Введение: зачем нужны функции активации
Без функции активации нейронная сеть — просто линейная регрессия, сколько бы слоёв ни было.
Без активации:
y = W2 * (W1 * x + b1) + b2
y = W2*W1*x + W2*b1 + b2
y = W'*x + b' ← один линейный слой!
С активацией:
y = W2 * σ(W1 * x + b1) + b2
← нелинейность! модель может аппроксимировать любые функции
Уровень 1: Step Function (исторический)
Первая нейронная сеть (McCulloch-Pitts, 1943)
Step(x) = 1, если x >= 0
= 0, если x < 0
Проблемы:
- Не дифференцируема (нет градиента в 0)
- Градиент = 0 везде кроме одной точки
- Обучение через градиенты невозможно
Уровень 2: Sigmoid
Сигмоида: от 0 до 1
def sigmoid(x):
return 1 / (1 + torch.exp(-x))
# Свойства:
# sigmoid(0) = 0.5
# sigmoid(∞) = 1.0
# sigmoid(-∞) = 0.0
# derivative = sigmoid(x) * (1 - sigmoid(x))
Визуализация
x: -5 -3 -1 0 1 3 5
sigmoid: 0.01 0.05 0.27 0.50 0.73 0.95 0.99
Проблема: vanishing gradients
sigmoid'(x) = sigmoid(x) * (1 - sigmoid(x))
При x = 5: sigmoid'(5) = 0.99 * 0.01 = 0.0099 ← почти 0!
При x = -5: sigmoid'(-5) = 0.01 * 0.99 = 0.0099 ← почти 0!
В глубокой сети (100 слоёв):
общий_градиент = 0.01^100 = 10^-200 ← градиент затух!
Где используется sigmoid?
+ Вероятности в output layer (бинарная классификация)
+ Gate mechanisms в LSTM (input gate, forget gate, output gate)
- Скрытые слои в больших моделях (vanishing gradients)
# LSTM gates используют sigmoid
class LSTMCell(nn.Module):
def forward(self, x, h_prev, c_prev):
# Все gates используют sigmoid → output в [0, 1]
forget_gate = sigmoid(W_f @ [x, h_prev] + b_f)
input_gate = sigmoid(W_i @ [x, h_prev] + b_i)
output_gate = sigmoid(W_o @ [x, h_prev] + b_o)
# Candidate использует tanh
candidate = tanh(W_c @ [x, h_prev] + b_c)
c_new = forget_gate * c_prev + input_gate * candidate
h_new = output_gate * tanh(c_new)
Уровень 3: Tanh
Гиперболический тангенс
def tanh(x):
return (torch.exp(x) - torch.exp(-x)) / (torch.exp(x) + torch.exp(-x))
# или проще:
return torch.tanh(x)
# Свойства:
# tanh(0) = 0
# tanh(∞) = 1.0
# tanh(-∞) = -1.0
# derivative = 1 - tanh(x)^2
Визуализация
x: -5 -3 -1 0 1 3 5
tanh: -0.99 -0.99 -0.76 0.00 0.76 0.99 0.99
Sigmoid vs Tanh
| Sigmoid | Tanh
----------------|---------------------|---------------------
Диапазон | [0, 1] | [-1, 1]
Центрирован | Нет (mean≈0.5) | Да (mean=0)
Градиент | max 0.25 | max 1.0
Использование | gates, output | скрытые слои (мало)
Tanh лучше sigmoid:
+ Zero-centered (лучше для градиентов)
+ Сильнее градиент (max=1 vs max=0.25)
- Всё ещё vanishing gradients
Уровень 4: ReLU
Rectified Linear Unit — прорыв 2011
def relu(x):
return torch.max(x, 0)
# или: return x.clamp_min(0)
# Свойства:
# relu(x) = x, если x > 0
# relu(x) = 0, если x <= 0
# derivative = 1, если x > 0
# derivative = 0, если x <= 0
Визуализация
x: -3 -1 0 1 3
relu: 0 0 0 1 3
derivative:
x <= 0: 0
x > 0: 1
Почему ReLU так хорош?
1. Не линейная по всему диапазону:
ReLU(x) = 0 для x < 0 → нелинейность!
2. Градиент = 1 для x > 0:
Нет vanishing gradients для положительных значений!
1^100 = 1 ← градиент течёт через 100 слоёв
3. Вычислительно эффективна:
relu(x) = max(x, 0) ← одно сравнение!
sigmoid(x) = 1 / (1 + exp(-x)) ← exp, деление
4. Разреженность:
50% нейронов "выключены" (output=0)
→ Регуляризация, меньше overfitting
Проблема: Dying ReLU
Проблема:
Если нейрон всегда output < 0 → gradient = 0 → weights не обновляются
Нейрон "умирает" и никогда не оживёт
Пример:
x = [-2, -3, -1, -4, -2, ...] ← все отрицательные
relu(x) = [0, 0, 0, 0, 0, ...]
gradient = 0 ← нейрон мёртв
Решение:
- Меньший learning rate
- Leaky ReLU / Parametric ReLU
Уровень 5: Leaky ReLU и варианты
Leaky ReLU
def leaky_relu(x, alpha=0.01):
return x if x > 0 else alpha * x
# Свойства:
# leaky_relu(x) = x, если x > 0
# leaky_relu(x) = 0.01 * x, если x <= 0
# derivative = 1, если x > 0
# derivative = 0.01, если x <= 0
Parametric ReLU (PReLU)
class PReLU(nn.Module):
def __init__(self, channels):
super().__init__()
# alpha обучается!
self.alpha = nn.Parameter(torch.ones(channels) * 0.25)
def forward(self, x):
return torch.where(x > 0, x, self.alpha * x)
Сравнение ReLU вариантов
Метод | x > 0 | x <= 0 | "Мёртвые" нейроны
--------------|-------------|-------------|------------------
ReLU | x | 0 | Да (проблема)
Leaky ReLU | x | 0.01*x | Нет
PReLU | x | α*x (learn)| Нет
ELU | x | α*(e^x-1) | Нет
GELU | x*Φ(x) | x*Φ(x) | Нет (очень мало)
GELU: Gaussian Error Linear Unit
GELU — функция активации BERT и GPT
def gelu(x):
# GELU(x) = x * Φ(x) где Φ(x) — CDF стандартного нормального
return x * 0.5 * (1.0 + torch.erf(x / math.sqrt(2.0)))
# Аппроксимация (используется в большинстве реализаций):
def gelu_approx(x):
return 0.5 * x * (1.0 + torch.tanh(
math.sqrt(2.0 / math.pi) * (x + 0.044715 * x**3)
))
Почему GELU лучше ReLU?
ReLU:
output = x, если x > 0
output = 0, если x <= 0
← жёсткий порог, не плавно
GELU:
output ≈ x, если x >> 0
output ≈ 0, если x << 0
output ≈ x/2, если x ≈ 0
← плавный переход, стохастический по природе
GELU(x) = x * P(x >= 0 | x)
→ Как будто dropout с вероятностью, зависящей от x
Визуализация GELU
x: -3 -1 -0.5 0 0.5 1 3
GELU: -1.0 -0.35 -0.22 0 0.25 0.65 2.5
ReLU: 0 0 0 0 0.5 1 3
GELU для малых x ≠ 0!
→ Малые значения не обрезаются полностью
→ Больше информации проходит через слой
SwiGLU: современная функция для LLM
GLU: Gate Linear Unit
class GLU(nn.Module):
def forward(self, x):
# Разделяем пополам: одна часть — input, другая — gate
input_part, gate = x.chunk(2, dim=-1)
return input_part * sigmoid(gate)
SwiGLU: Swish + GLU
class SwiGLU(nn.Module):
def __init__(self, hidden_size, ff_size):
super().__init__()
self.w1 = nn.Linear(hidden_size, ff_size) # Linear
self.w2 = nn.Linear(hidden_size, ff_size) # Linear
self.w3 = nn.Linear(ff_size, hidden_size) # Linear
def forward(self, x):
# SwiGLU(x) = SwiGLU(w1(x)) * w2(x)
swish = self.w1(x) * sigmoid(self.w1(x)) # Swish = x * sigmoid(x)
return self.w3(swish * self.w2(x)) # GLU = input * sigmoid(gate)
Где используется SwiGLU?
Модель | Функция активации
-----------------|---------------------------
BERT | GELU
GPT-2 | GeLU (approx)
GPT-3 | GeLU
Llama 1, 2 | SwiGLU
Llama 3 | SwiGLU
Mistral | SwiGLU
Mixtral | SwiGLU
PaLM | SwiGLU
SwiGLU стал стандартом для всех современных LLM!
Почему SwiGLU лучше?
1. Gate mechanism (как в GLU):
→ Динамически фильтрует информацию
→ Лучше обучается
2. Swish активация:
→ Нелинейность для малых значений
→ Плавный переход
3. Два линейных слоя (w1, w2):
→ Больше параметров при том же ff_size
→ Лучше качество
Сравнение функций активации
Метод | Скорость | Градиент | Разреженность | LLM
---------|----------|----------|---------------|----
Sigmoid | Медленно | Затухает | Высокая | Нет
Tanh | Средне | Затухает | Низкая | Нет
ReLU | Быстро | 1 или 0 | 50% | Редко
LeakyReLU| Быстро | 1 или α | Низкая | Иногда
GELU | Средне | Плавный | Низкая | BERT, GPT
SwiGLU | Медленнее| Плавный | Низкая | Llama, Mistral
Практические аспекты
Выбор функции активации
Для Transformer decoder:
→ SwiGLU (стандарт для LLM)
Для encoder:
→ GELU (хороший баланс)
Для быстрых моделей:
→ ReLU (просто и быстро)
Для LSTM:
→ sigmoid + tanh
Влияние на обучение
С ReLU:
loss = [10.2, 7.5, 6.1, 5.3, ...] ← может быть нестабильно
С GELU:
loss = [10.2, 7.2, 5.8, 5.0, ...] ← плавнее
С SwiGLU:
loss = [10.2, 6.8, 5.2, 4.3, ...] ← лучше качество, но медленнее
Квантование и активации
При квантовании:
ReLU: легко квантуется (max(0, x) → clamp)
GELU: сложнее (нужна аппроксимация)
SwiGLU: sigmoid сложно квантовать
Решение:
Выполнять активации в float32
Квантовать только linear weights
Итоги
- Без функции активации нейросеть — просто линейная модель
- Sigmoid и tanh страдают от vanishing gradients
- ReLU — прорыв: простой, быстрый, без vanishing gradients
- GELU — лучше для BERT и GPT-подобных моделей
- SwiGLU — стандарт для всех современных LLM (Llama, Mistral)
- Выбор функции активации влияет на качество и скорость обучения