Фаза 03 · урок 04
Функции активации
Цель урока: Сложите два линейных преобразования: y = W2(W1x + b1) + b2 . Раскроем: y = W2W1x + W2b1 + b2 . Это всего лишь y = Ax + c — одно линейное преобразование. Сколько бы линейных слоёв вы ни сложили, результат сворачивается в одно умножение…
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- Почему необходима нелинейность
- Сигмоида
- Tanh
- ReLU: прорыв
- Leaky ReLU
- GELU: современный вариант по умолчанию
- Swish / SiLU
- Softmax: выходная активация
- Сравнение форм
- Сравнение потока градиентов
- Какую активацию когда использовать
- Соберите это
- Шаг 1: реализуйте все функции активации с производными
- Шаг 2: визуализируйте, где умирают градиенты
- Шаг 3: эксперимент с затухающими градиентами
- Шаг 4: детектор мёртвых нейронов
- Шаг 5: сравнение обучения — сигмоида, ReLU и GELU
- Используйте это
- Выпустите это
- Упражнения
- Ключевые термины
- Дополнительное чтение
Без нелинейности ваша сеть из 100 слоёв — просто модное умножение матриц. Активации — это ворота, которые позволяют нейронным сетям мыслить кривыми.
Тип: Сборка Языки: Python Предварительные требования: Урок 03.03 (Обратное распространение ошибки) Время: ~75 минут
Цели обучения
- Реализовать с нуля сигмоиду,
tanh, ReLU, Leaky ReLU, GELU, Swish и softmax вместе с их производными - Диагностировать проблему затухающих градиентов, измеряя величины активаций в 10+ слоях с разными функциями активации
- Обнаруживать мёртвые нейроны в сети с ReLU и объяснять, почему GELU избегает этого режима отказа
- Выбирать правильную функцию активации для конкретной архитектуры (transformer, CNN, RNN, выходной слой)
Проблема
Сложите два линейных преобразования: y = W2(W1x + b1) + b2. Раскроем: y = W2W1x + W2b1 + b2. Это всего лишь y = Ax + c — одно линейное преобразование. Сколько бы линейных слоёв вы ни сложили, результат сворачивается в одно умножение матриц. Ваша сеть из 100 слоёв обладает той же выразительной силой, что и один слой.
Это не теоретическая мелочь. Это означает, что глубокая линейная сеть буквально не может выучить XOR, классифицировать спиральный набор данных или распознать лицо. Без функций активации глубина — иллюзия.
Функции активации разрушают линейность. Они преобразуют выход каждого слоя нелинейной функцией, давая сети способность искривлять границы решений, аппроксимировать произвольные функции и действительно обучаться. Но выберите неподходящую активацию — и ваши градиенты затухнут до нуля (сигмоида в глубоких сетях), взорвутся до бесконечности (неограниченные активации без аккуратной инициализации), либо нейроны навсегда умрут (ReLU с большими отрицательными смещениями). Выбор функции активации напрямую определяет, будет ли сеть вообще обучаться.
Концепция
Почему необходима нелинейность
Умножение матриц композиционно. Умножение вектора сначала на матрицу A, а затем на матрицу B идентично умножению на AB. Поэтому стек из десяти линейных слоёв математически эквивалентен одному линейному слою с одной большой матрицей. Все эти параметры, вся эта глубина — впустую. Нужен механизм, разрывающий цепочку. Именно это делают функции активации.
Вот доказательство. Линейный слой вычисляет f(x) = Wx + b. Сложим два:
Layer 1: h = W1 * x + b1
Layer 2: y = W2 * h + b2
Подставим:
y = W2 * (W1 * x + b1) + b2
y = (W2 * W1) * x + (W2 * b1 + b2)
y = A * x + c
Один слой. Вставьте нелинейную активацию g() между слоями:
h = g(W1 * x + b1)
y = W2 * h + b2
Теперь подстановка нарушается. W2 * g(W1 * x + b1) + b2 нельзя свести к одному линейному преобразованию. Сеть может представлять нелинейные функции. Каждый дополнительный слой с активацией добавляет выразительную способность.
Сигмоида
Исходная функция активации для нейронных сетей.
sigmoid(x) = 1 / (1 + e^(-x))
Диапазон выхода: (0, 1). Гладкая, дифференцируемая, отображает любое вещественное число в значение, похожее на вероятность.
Производная:
sigmoid'(x) = sigmoid(x) * (1 - sigmoid(x))
Максимальное значение этой производной — 0.25, оно достигается при x = 0. При обратном распространении градиенты перемножаются при прохождении через слои. Десять слоёв с сигмоидой означают, что градиент умножается максимум на 0.25 десять раз:
0.25^10 = 0.000000953674
Это меньше одной миллионной исходного сигнала. Так возникает проблема затухающих градиентов. Градиенты в ранних слоях становятся настолько малы, что веса почти не обновляются. Сеть выглядит обучающейся — потери уменьшаются в поздних слоях, — но первые слои заморожены. Глубокие сети с сигмоидой попросту не обучаются.
Дополнительная проблема: выходы сигмоиды всегда положительны (от 0 до 1), то есть градиенты весов всегда имеют один знак. Это вызывает зигзагообразное движение при градиентном спуске.
Tanh
Центрированная версия сигмоиды.
tanh(x) = (e^x - e^(-x)) / (e^x + e^(-x))
Диапазон выхода: (-1, 1). Центрирована относительно нуля, что устраняет проблему зигзага.
Производная:
tanh'(x) = 1 - tanh(x)^2
Максимальная производная равна 1.0 при x = 0 — в четыре раза лучше сигмоиды. Но проблема затухающих градиентов всё ещё существует. Для больших положительных или отрицательных входов производная стремится к нулю. Десять слоёв всё ещё подавляют градиент, просто менее агрессивно.
ReLU: прорыв
Rectified Linear Unit. Популяризирована в глубоком обучении Наиром и Хинтоном в 2010 году (сама функция восходит к работе Фукусимы 1969 года) и изменила всё.
relu(x) = max(0, x)
Диапазон выхода: [0, infinity). Производная тривиально проста:
relu'(x) = 1 if x > 0
0 if x <= 0
Для положительных входов затухающих градиентов нет. Градиент в точности равен 1 и передаётся прямо через слой. Поэтому глубокие сети стали обучаемыми: ReLU сохраняет величину градиента между слоями.
Но есть режим отказа: проблема мёртвых нейронов. Если взвешенный вход нейрона всегда отрицателен (из-за большого отрицательного смещения или неудачной инициализации весов), его выход всегда ноль, градиент всегда ноль, и он никогда не обновляется. Он умирает навсегда. На практике во время обучения может погибнуть 10–40 % нейронов сети с ReLU.
Leaky ReLU
Самое простое исправление мёртвых нейронов.
leaky_relu(x) = x if x > 0
alpha * x if x <= 0
Здесь alpha — небольшая константа, обычно 0.01. У отрицательной части есть небольшой наклон вместо нуля, поэтому мёртвые нейроны всё ещё получают градиентный сигнал и могут восстановиться.
GELU: современный вариант по умолчанию
Gaussian Error Linear Unit. Представлена Хендриксом и Гимпелем в 2016 году. Активация по умолчанию в BERT, GPT и большинстве современных transformer-моделей.
gelu(x) = x * Phi(x)
Здесь Phi(x) — функция распределения стандартной нормальной величины. Практически используемая аппроксимация:
gelu(x) ~= 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))
GELU гладка всюду, допускает небольшие отрицательные значения (в отличие от ReLU, жёстко обрезающей их до нуля) и имеет вероятностную интерпретацию: она взвешивает каждый вход вероятностью того, что он положителен при гауссовом распределении. Такое гладкое пропускание превосходит ReLU в transformer-архитектурах, поскольку обеспечивает лучший поток градиентов и полностью избегает проблемы мёртвых нейронов.
Swish / SiLU
Самоуправляемая активация, открытая Рамачандраном и соавторами в 2017 году автоматизированным поиском.
swish(x) = x * sigmoid(x)
Формально Swish — это x * sigmoid(x). Google обнаружил её автоматизированным поиском в пространстве функций активации: нейронная сеть проектировала части нейронных сетей.
Как и GELU, она гладкая, немонотонная и допускает небольшие отрицательные значения. Различие тонкое: Swish использует сигмоиду для пропускания, а GELU — гауссову CDF. На практике производительность почти одинакова. Swish применяется в EfficientNet и некоторых моделях компьютерного зрения. В языковых моделях преобладает GELU.
Softmax: выходная активация
Не используется в скрытых слоях. Softmax преобразует вектор необработанных оценок (логитов) в распределение вероятностей.
softmax(x_i) = e^(x_i) / sum(e^(x_j) for all j)
Каждый выход находится между 0 и 1. Сумма всех выходов равна 1. Поэтому это стандартная финальная активация для многоклассовой классификации. Наибольший логит получает наибольшую вероятность, но, в отличие от argmax, softmax дифференцируема и сохраняет информацию об относительной уверенности.
Сравнение форм
Сравнение потока градиентов
Какую активацию когда использовать
softmax-temperature
Соберите это
Шаг 1: реализуйте все функции активации с производными
Каждая функция принимает одно число с плавающей точкой и возвращает число с плавающей точкой. Каждая функция производной принимает тот же вход и возвращает градиент.
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]
Шаг 2: визуализируйте, где умирают градиенты
Вычислите градиент в 100 равномерно расположенных точках от -5 до 5. Выведите текстовую гистограмму, показывающую, где градиент каждой активации близок к нулю.
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)
Шаг 3: эксперимент с затухающими градиентами
Выполните прямой проход сигнала через N слоёв с сигмоидой и ReLU. Измерьте, как меняется величина активации.
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")
Шаг 4: детектор мёртвых нейронов
Создайте сеть с ReLU, пропустите через неё случайные входы и посчитайте, сколько нейронов ни разу не сработали.
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()
Шаг 5: сравнение обучения — сигмоида, ReLU и GELU
Обучите одну и ту же двухслойную сеть на наборе данных «круг» (точки внутри круга = класс 1, снаружи = класс 0) с тремя разными активациями. Сравните скорость сходимости.
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}%)")
Используйте это
PyTorch предоставляет всё это и в функциональной форме, и в виде модулей:
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),
)
Скрытые слои transformer: GELU. Скрытые слои CNN: ReLU. Выходной слой для классификации: softmax. Выходной слой для регрессии: ничего (линейный). Выходной слой для вероятностей: сигмоида. Вот и всё. Начинайте с этих вариантов по умолчанию. Меняйте их, только когда у вас есть основания.
RNN и LSTM используют tanh для скрытого состояния и сигмоиду для ворот, но если вы сегодня создаёте модель с нуля, вы, вероятно, не используете RNN. Если нейроны умирают в вашей сети с ReLU, переключитесь на GELU. Не тянитесь к Leaky ReLU без конкретной причины: GELU решает проблему мёртвых нейронов и обеспечивает лучший поток градиентов.
Выпустите это
Этот урок создаёт:
outputs/prompt-activation-selector.md— повторно используемый промпт, помогающий выбрать правильную функцию активации для любой архитектуры
Упражнения
-
Реализуйте Parametric ReLU (PReLU), где отрицательный наклон
alpha— обучаемый параметр. Обучите её на наборе данных «круг» и сравните с фиксированной Leaky ReLU. -
Запустите эксперимент с затухающими градиентами с 50 слоями вместо 10. Постройте график величины на каждом слое для сигмоиды,
tanh, ReLU и GELU. На каком слое сигнал каждой активации фактически достигает нуля? -
Реализуйте ELU (Exponential Linear Unit):
elu(x) = x if x > 0, alpha * (e^x - 1) if x <= 0. Сравните долю мёртвых нейронов с ReLU в той же сети. -
Создайте «монитор здоровья градиентов», работающий во время обучения: в каждой эпохе вычисляйте среднюю величину градиента в каждом слое. Выводите предупреждение, когда градиент любого слоя падает ниже
0.001или превышает100. -
Измените сравнение обучения, чтобы использовать набор данных XOR из урока 01 вместо кругов. Какая активация быстрее всего сходится на XOR? Почему это отличается от результатов на круге?
Ключевые термины
| Термин | Как обычно говорят | Что это действительно означает |
|---|---|---|
| Функция активации | «Нелинейная часть» | Функция, применяемая к выходу каждого нейрона и разрушающая линейность, чтобы сеть могла учить нелинейные отображения |
| Затухающий градиент | «В глубоких сетях исчезают градиенты» | Градиенты экспоненциально уменьшаются при прохождении через слои, когда производная активации меньше 1, из-за чего ранние слои невозможно обучить |
| Взрывающийся градиент | «Градиенты взрываются» | Градиенты экспоненциально растут при прохождении через слои, когда эффективный множитель превышает 1, что делает обучение нестабильным |
| Мёртвый нейрон | «Нейрон перестал учиться» | Нейрон ReLU, чей вход постоянно отрицателен, из-за чего выход и градиент равны нулю |
| Сигмоида | «Сжимает значения до 0–1» | Логистическая функция 1/(1+e^-x), исторически важная, но вызывающая затухание градиентов в глубоких сетях |
| ReLU | «Обрезает отрицательные значения до нуля» | max(0, x) — активация, сделавшая глубокое обучение практичным благодаря сохранению величины градиента |
| GELU | «Активация для transformer» | Gaussian Error Linear Unit, гладкая активация, взвешивающая входы их вероятностью быть положительными |
| Swish/SiLU | «Самоуправляемая ReLU» | x * sigmoid(x), найденная автоматизированным поиском и применяемая в EfficientNet |
| Softmax | «Преобразует оценки в вероятности» | Нормализует вектор логитов в распределение вероятностей, где все значения принадлежат (0,1) и суммируются до 1 |
| Leaky ReLU | «ReLU, которая не умирает» | max(alpha*x, x), где alpha мало (0.01); предотвращает смерть нейронов небольшими отрицательными градиентами |
| Насыщение | «Плоская часть сигмоиды» | Области, в которых производная активации стремится к нулю и блокирует поток градиента |
| Логит | «Необработанная оценка перед softmax» | Ненормализованный выход последнего слоя перед применением softmax или сигмоиды |
Дополнительное чтение
- Nair & Hinton, «Rectified Linear Units Improve Restricted Boltzmann Machines» (2010) — статья, представившая ReLU и сделавшая возможным обучение глубоких сетей
- Hendrycks & Gimpel, «Gaussian Error Linear Units (GELUs)» (2016) — представила функцию активации, ставшую вариантом по умолчанию для transformer-моделей
- Ramachandran и соавт., «Searching for Activation Functions» (2017) — использовала автоматизированный поиск для обнаружения Swish и показала, что проектирование активаций можно автоматизировать
- Glorot & Bengio, «Understanding the difficulty of training deep feedforward neural networks» (2010) — статья, диагностировавшая затухающие/взрывающиеся градиенты и предложившая инициализацию Xavier
- Goodfellow, Bengio, Courville, «Deep Learning», глава 6.3 (https://www.deeplearningbook.org/) — строгое изложение скрытых единиц и функций активации
Источник: Activation Functions 03.03 — Обратное распространение ошибки с нуля · Фаза 3 — Основы глубокого обучения · Полный каталог · 03.05 — Функции потерь