Перейти к содержанию

Функции активации

Введение

Функции активации играют критическую роль в нейронных сетях, внося нелинейность, которая позволяет сетям обучаться сложным зависимостям. Без функций активации нейронная сеть была бы просто линейной комбинацией входов, независимо от количества слоёв.

Зачем нужны функции активации?

Нелинейность

Если бы мы использовали только линейные преобразования:

y = W₂(W₁x + b₁) + b₂ = W'x + b'

Многослойная сеть выродилась бы в один слой. Функции активации ломают эту линейность:

y = f(W₂f(W₁x + b₁) + b₂)

Свойства хороших функций активации

  1. Нелинейность - позволяет аппроксимировать сложные функции
  2. Дифференцируемость - необходима для обратного распространения
  3. Монотонность - упрощает оптимизацию (опционально)
  4. Ограниченный диапазон - помогает стабилизировать обучение
  5. Вычислительная эффективность - важна для больших сетей

Основные функции активации

Sigmoid (Логистическая функция)

σ(x) = 1 / (1 + e^(-x))

Характеристики: - Диапазон: (0, 1) - Производная: σ'(x) = σ(x)(1 - σ(x)) - Максимум производной: 0.25 при x = 0

Преимущества: - Интерпретируемый выход (вероятность) - Гладкая и монотонная - Хорошо работает для бинарной классификации

Недостатки: - Проблема исчезающих градиентов - Выход не центрирован вокруг нуля - Вычислительно дороже из-за экспоненты

Применение: - Выходной слой для бинарной классификации - Редко в скрытых слоях современных сетей

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    s = sigmoid(x)
    return s * (1 - s)

Tanh (Гиперболический тангенс)

tanh(x) = (e^x - e^(-x)) / (e^x + e^(-x))

Характеристики: - Диапазон: (-1, 1) - Производная: tanh'(x) = 1 - tanh²(x) - Центрирована вокруг нуля

Преимущества: - Сильнее сигмоиды (градиенты ближе к 1) - Центрированный выход улучшает сходимость - Гладкая и монотонная

Недостатки: - Всё ещё проблема исчезающих градиентов - Вычислительно затратная

Применение: - Скрытые слои в небольших сетях - RNN и LSTM (часто предпочтительнее ReLU)

def tanh(x):
    return np.tanh(x)

def tanh_derivative(x):
    return 1 - np.tanh(x) ** 2

ReLU (Rectified Linear Unit)

ReLU(x) = max(0, x)

Характеристики: - Диапазон: [0, ∞) - Производная: 1 при x > 0, 0 при x < 0 - Не определена при x = 0 (обычно принимают 0 или 1)

Преимущества: - Вычислительно эффективная - Решает проблему исчезающих градиентов для положительных значений - Разреженная активация (многие нейроны "молчат") - Быстрая сходимость на практике

Недостатки: - "Мёртвые нейроны" (Dying ReLU) - нейроны могут перестать активироваться - Не дифференцируема в точке 0 - Не центрирована вокруг нуля

Применение: - Наиболее популярная функция для скрытых слоёв - CNN, fully connected сети

def relu(x):
    return np.maximum(0, x)

def relu_derivative(x):
    return (x > 0).astype(float)

Leaky ReLU

LeakyReLU(x) = { x, если x > 0
               { αx, если x ≤ 0

где α - небольшой коэффициент (обычно 0.01)

Характеристики: - Диапазон: (-∞, ∞) - Небольшой градиент для отрицательных значений

Преимущества: - Решает проблему мёртвых нейронов - Все преимущества ReLU - Лучшая производительность в некоторых задачах

Недостатки: - Нужно подбирать параметр α - Не всегда лучше обычного ReLU

def leaky_relu(x, alpha=0.01):
    return np.where(x > 0, x, alpha * x)

def leaky_relu_derivative(x, alpha=0.01):
    return np.where(x > 0, 1, alpha)

Parametric ReLU (PReLU)

Вариация Leaky ReLU, где α обучается вместе с другими параметрами:

class PReLU:
    def __init__(self, alpha=0.01):
        self.alpha = alpha

    def forward(self, x):
        return np.where(x > 0, x, self.alpha * x)

    def backward(self, x, grad_output):
        grad_input = np.where(x > 0, grad_output, self.alpha * grad_output)
        # Градиент по alpha можно использовать для обновления
        return grad_input

Exponential Linear Unit (ELU)

ELU(x) = { x, если x > 0
         { α(e^x - 1), если x ≤ 0

Характеристики: - Плавный переход для отрицательных значений - Среднее значение активаций ближе к нулю

Преимущества: - Решает проблему мёртвых нейронов - Плавные градиенты - Часто показывает лучшую точность чем ReLU

Недостатки: - Вычислительно дороже из-за экспоненты - Нужно подбирать α

def elu(x, alpha=1.0):
    return np.where(x > 0, x, alpha * (np.exp(x) - 1))

def elu_derivative(x, alpha=1.0):
    return np.where(x > 0, 1, alpha * np.exp(x))

SELU (Scaled ELU)

SELU(x) = λ * ELU(x, α)

где λ ≈ 1.0507, α ≈ 1.6733

Особенности: - Самонормализующиеся свойства - Поддерживает среднее и дисперсию активаций - Работает только с правильной инициализацией LeCun

Применение: - Полносвязные сети - Требует использования Dropout variant AlphaDropout

def selu(x):
    alpha = 1.6732632423543772848170429916717
    scale = 1.0507009873554804934193349852946
    return scale * np.where(x > 0, x, alpha * (np.exp(x) - 1))

Softmax

softmax(x)_i = e^(x_i) / Σⱼ e^(x_j)

Характеристики: - Преобразует logits в вероятности - Сумма выходов равна 1 - Используется только в выходном слое

Применение: - Многоклассовая классификация - Всегда в сочетании с cross-entropy loss

def softmax(x):
    exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))  # для численной стабильности
    return exp_x / np.sum(exp_x, axis=-1, keepdims=True)

Важно: На практике softmax часто комбинируется с cross-entropy loss для численной стабильности.

Swish (SiLU)

Swish(x) = x * σ(x) = x / (1 + e^(-x))

Характеристики: - Нemonotonic (не монотонная) - Гладкая везде - Самоограничивающаяся

Преимущества: - Часто превосходит ReLU в глубоких сетях - Используется в EfficientNet, Transformer моделях

def swish(x):
    return x * sigmoid(x)

GELU (Gaussian Error Linear Unit)

GELU(x) = x * Φ(x)

где Φ(x) - CDF стандартного нормального распределения

Аппроксимация:

GELU(x) ≈ 0.5x * (1 + tanh(√(2/π) * (x + 0.044715x³)))

Применение: - Transformer модели (BERT, GPT) - State-of-the-art результаты во многих задачах

import math

def gelu(x):
    return 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x ** 3)))

Сравнение функций активации

Функция Диапазон Центрирована Вычислительная стоимость Градиенты
Sigmoid (0, 1) Нет Средняя Исчезают
Tanh (-1, 1) Да Средняя Исчезают
ReLU [0, ∞) Нет Низкая Мёртвые нейроны
Leaky ReLU (-∞, ∞) Нет Низкая Есть всегда
ELU (-α, ∞) Близко Высокая Есть всегда
SELU (-λα, ∞) Да Высокая Самонормализация
Swish (-∞, ∞) Нет Высокая Отличные
GELU (-∞, ∞) Нет Высокая Отличные

Рекомендации по выбору

Для скрытых слоёв

  1. По умолчанию: ReLU
  2. Быстрая, простая, хорошо работает

  3. Если мёртвые нейроны: Leaky ReLU или ELU

  4. Особенно в очень глубоких сетях

  5. Для state-of-the-art: Swish или GELU

  6. Transformer архитектуры
  7. Когда важна максимальная точность

  8. Для самонормализации: SELU

  9. Только с инициализацией LeCun
  10. Без Batch Normalization

Для выходного слоя

  1. Бинарная классификация: Sigmoid
  2. Многоклассовая классификация: Softmax
  3. Регрессия:
  4. Линейная (без активации) для произвольного диапазона
  5. ReLU для неотрицательных значений
  6. Sigmoid/Tanh для ограниченного диапазона

Проблемы и решения

Dying ReLU Problem

Проблема: Нейроны с ReLU могут "умереть" -永远输出 0

Причины: - Большой learning rate - Инициализация с большими отрицательными смещениями - Градиенты толкают веса в область отрицательных входов

Решения: - Использовать Leaky ReLU, ELU, или Swish - Уменьшить learning rate - Правильная инициализация весов - Batch Normalization

Численная стабильность

Для softmax используйте trick с вычитанием максимума:

def stable_softmax(x):
    shift_x = x - np.max(x, axis=-1, keepdims=True)
    exp_x = np.exp(shift_x)
    return exp_x / np.sum(exp_x, axis=-1, keepdims=True)

Практический пример

import numpy as np
import matplotlib.pyplot as plt

def compare_activations():
    x = np.linspace(-5, 5, 1000)

    activations = {
        'Sigmoid': sigmoid(x),
        'Tanh': np.tanh(x),
        'ReLU': relu(x),
        'Leaky ReLU': leaky_relu(x),
        'ELU': elu(x),
        'Swish': swish(x)
    }

    fig, axes = plt.subplots(1, 2, figsize=(14, 5))

    # Функции
    for name, y in activations.items():
        axes[0].plot(x, y, label=name)
    axes[0].set_title('Функции активации')
    axes[0].set_xlabel('x')
    axes[0].set_ylabel('f(x)')
    axes[0].legend()
    axes[0].grid(True)

    # Производные
    derivatives = {
        'Sigmoid': sigmoid_derivative(x),
        'Tanh': 1 - np.tanh(x) ** 2,
        'ReLU': relu_derivative(x),
        'Leaky ReLU': leaky_relu_derivative(x),
        'ELU': elu_derivative(x)
    }

    for name, y in derivatives.items():
        axes[1].plot(x, y, label=name)
    axes[1].set_title('Производные функций активации')
    axes[1].set_xlabel('x')
    axes[1].set_ylabel("f'(x)")
    axes[1].legend()
    axes[1].grid(True)

    plt.tight_layout()
    plt.show()

Заключение

Выбор функции активации зависит от конкретной задачи:

  • ReLU - отличный выбор по умолчанию для большинства задач
  • Leaky ReLU/ELU - если возникают проблемы с мёртвыми нейронами
  • Swish/GELU - для достижения state-of-the-art результатов
  • Sigmoid/Softmax - для выходных слоёв классификации

Экспериментируйте с разными функциями активации и выбирайте ту, которая даёт лучшие результаты на валидационном наборе данных.

Дополнительные ресурсы